-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsession.ts
More file actions
126 lines (106 loc) · 4.02 KB
/
session.ts
File metadata and controls
126 lines (106 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// Uses Node.js 22+ built-in sqlite — no native compilation, no npm package required
import { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";
import { mkdirSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { Config } from "./config.js";
import { runMigrations } from "./migrations.js";
export type EventType = "file_write" | "file_edit" | "task_complete" | "error" | "fetch" | "session_ended";
export interface SessionEvent {
event_type: EventType;
file_path?: string;
task_name?: string;
error_type?: string;
created_at?: string;
// SECURITY: No content/output fields — we never store file contents or command output
}
function getEventLogPath(projectPath: string): string {
const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
return join(Config.DB_DIR, `${hash}.events.jsonl`);
}
function dbPath(projectPath: string): string {
const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
return join(Config.DB_DIR, `${hash}.db`);
}
function openDb(projectPath: string): DatabaseSync {
mkdirSync(Config.DB_DIR, { recursive: true });
const db = new DatabaseSync(dbPath(projectPath));
// WAL mode for concurrent multi-agent access + busy wait instead of immediate fail
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA busy_timeout = 5000");
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
last_active TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
event_type TEXT NOT NULL,
file_path TEXT,
task_name TEXT,
error_type TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY(session_id) REFERENCES sessions(id)
);
`);
// Apply all pending schema migrations
runMigrations(db);
return db;
}
export function getOrCreateSession(projectPath: string): number {
const db = openDb(projectPath);
const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
const now = new Date().toISOString();
type Row = { id: number };
const existing = db.prepare(
"SELECT id FROM sessions WHERE project_hash = ? AND last_active > ? ORDER BY last_active DESC LIMIT 1"
).get(hash, new Date(Date.now() - 86_400_000).toISOString()) as Row | undefined;
if (existing) {
db.prepare("UPDATE sessions SET last_active = ? WHERE id = ?").run(now, existing.id);
db.close();
return existing.id;
}
const result = db.prepare(
"INSERT INTO sessions(project_hash, created_at, last_active) VALUES (?, ?, ?)"
).run(hash, now, now) as { lastInsertRowid: number | bigint };
db.close();
return Number(result.lastInsertRowid);
}
export function recordEvent(projectPath: string, event: SessionEvent): void {
const db = openDb(projectPath);
const sessionId = getOrCreateSession(projectPath);
db.prepare(
`INSERT INTO events(session_id, event_type, file_path, task_name, error_type, created_at)
VALUES (?, ?, ?, ?, ?, ?)`
).run(
sessionId,
event.event_type,
event.file_path ?? null,
event.task_name ?? null,
event.error_type ?? null,
new Date().toISOString()
);
db.close();
}
export function getRecentEvents(projectPath: string, limit = 50): SessionEvent[] {
// Read from the JSONL event log written by hooks (posttooluse.mjs, stop.mjs).
// Hooks write minimal metadata events; this is the source of truth for zc_recall_context().
const logPath = getEventLogPath(projectPath);
if (!existsSync(logPath)) return [];
try {
const lines = readFileSync(logPath, "utf8").split("\n").filter(Boolean);
return lines
.slice(-limit)
.reverse()
.map((line) => {
try { return JSON.parse(line) as SessionEvent; }
catch { return null; }
})
.filter((e): e is SessionEvent => e !== null);
} catch {
return [];
}
}