@fml-inc/panopticon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +5 -0
- package/README.md +363 -0
- package/bin/hook-handler +3 -0
- package/bin/mcp-server +3 -0
- package/bin/panopticon +3 -0
- package/bin/proxy +3 -0
- package/bin/server +3 -0
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.js +48 -0
- package/dist/api/client.js.map +1 -0
- package/dist/chunk-3BUJ7URA.js +387 -0
- package/dist/chunk-3BUJ7URA.js.map +1 -0
- package/dist/chunk-3TZAKV3M.js +158 -0
- package/dist/chunk-3TZAKV3M.js.map +1 -0
- package/dist/chunk-4SM2H22C.js +169 -0
- package/dist/chunk-4SM2H22C.js.map +1 -0
- package/dist/chunk-7Q3BJMLG.js +62 -0
- package/dist/chunk-7Q3BJMLG.js.map +1 -0
- package/dist/chunk-BVOE7A2Z.js +412 -0
- package/dist/chunk-BVOE7A2Z.js.map +1 -0
- package/dist/chunk-CF4GPWLI.js +170 -0
- package/dist/chunk-CF4GPWLI.js.map +1 -0
- package/dist/chunk-DZ5HJFB4.js +467 -0
- package/dist/chunk-DZ5HJFB4.js.map +1 -0
- package/dist/chunk-HQCY722C.js +428 -0
- package/dist/chunk-HQCY722C.js.map +1 -0
- package/dist/chunk-HRCEIYKU.js +134 -0
- package/dist/chunk-HRCEIYKU.js.map +1 -0
- package/dist/chunk-K7YUPLES.js +76 -0
- package/dist/chunk-K7YUPLES.js.map +1 -0
- package/dist/chunk-L7G27XWF.js +130 -0
- package/dist/chunk-L7G27XWF.js.map +1 -0
- package/dist/chunk-LWXF7YRG.js +626 -0
- package/dist/chunk-LWXF7YRG.js.map +1 -0
- package/dist/chunk-NXH7AONS.js +1120 -0
- package/dist/chunk-NXH7AONS.js.map +1 -0
- package/dist/chunk-QK5442ZP.js +55 -0
- package/dist/chunk-QK5442ZP.js.map +1 -0
- package/dist/chunk-QVK6VGCV.js +1703 -0
- package/dist/chunk-QVK6VGCV.js.map +1 -0
- package/dist/chunk-RX2RXHBH.js +1699 -0
- package/dist/chunk-RX2RXHBH.js.map +1 -0
- package/dist/chunk-SEXU2WYG.js +788 -0
- package/dist/chunk-SEXU2WYG.js.map +1 -0
- package/dist/chunk-SUGSQ4YI.js +264 -0
- package/dist/chunk-SUGSQ4YI.js.map +1 -0
- package/dist/chunk-TGXFVAID.js +138 -0
- package/dist/chunk-TGXFVAID.js.map +1 -0
- package/dist/chunk-WLBNFVIG.js +447 -0
- package/dist/chunk-WLBNFVIG.js.map +1 -0
- package/dist/chunk-XLTCUH5A.js +1072 -0
- package/dist/chunk-XLTCUH5A.js.map +1 -0
- package/dist/chunk-YVRWVDIA.js +146 -0
- package/dist/chunk-YVRWVDIA.js.map +1 -0
- package/dist/chunk-ZEC4LRKS.js +176 -0
- package/dist/chunk-ZEC4LRKS.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1084 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-NwoZC-GM.d.ts +20 -0
- package/dist/db.d.ts +46 -0
- package/dist/db.js +15 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +37 -0
- package/dist/doctor.js +14 -0
- package/dist/doctor.js.map +1 -0
- package/dist/hooks/handler.d.ts +23 -0
- package/dist/hooks/handler.js +295 -0
- package/dist/hooks/handler.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +243 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/otlp/server.d.ts +7 -0
- package/dist/otlp/server.js +17 -0
- package/dist/otlp/server.js.map +1 -0
- package/dist/permissions.d.ts +33 -0
- package/dist/permissions.js +14 -0
- package/dist/permissions.js.map +1 -0
- package/dist/pricing.d.ts +29 -0
- package/dist/pricing.js +13 -0
- package/dist/pricing.js.map +1 -0
- package/dist/proxy/server.d.ts +10 -0
- package/dist/proxy/server.js +20 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/prune.d.ts +18 -0
- package/dist/prune.js +13 -0
- package/dist/prune.js.map +1 -0
- package/dist/query.d.ts +56 -0
- package/dist/query.js +27 -0
- package/dist/query.js.map +1 -0
- package/dist/reparse-636YZCE3.js +14 -0
- package/dist/reparse-636YZCE3.js.map +1 -0
- package/dist/repo.d.ts +17 -0
- package/dist/repo.js +9 -0
- package/dist/repo.js.map +1 -0
- package/dist/scanner.d.ts +73 -0
- package/dist/scanner.js +15 -0
- package/dist/scanner.js.map +1 -0
- package/dist/sdk.d.ts +82 -0
- package/dist/sdk.js +208 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +35 -0
- package/dist/setup.js +19 -0
- package/dist/setup.js.map +1 -0
- package/dist/sync/index.d.ts +29 -0
- package/dist/sync/index.js +32 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/targets.d.ts +279 -0
- package/dist/targets.js +20 -0
- package/dist/targets.js.map +1 -0
- package/dist/types-D-MYCBol.d.ts +128 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/hooks/hooks.json +274 -0
- package/package.json +124 -0
- package/skills/panopticon-optimize/SKILL.md +222 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
updateSessionMessageCounts,
|
|
3
|
+
upsertSession,
|
|
4
|
+
upsertSessionCwd,
|
|
5
|
+
upsertSessionRepository
|
|
6
|
+
} from "./chunk-BVOE7A2Z.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveRepoFromCwd
|
|
9
|
+
} from "./chunk-YVRWVDIA.js";
|
|
10
|
+
import {
|
|
11
|
+
log
|
|
12
|
+
} from "./chunk-7Q3BJMLG.js";
|
|
13
|
+
import {
|
|
14
|
+
refreshIfStale
|
|
15
|
+
} from "./chunk-3TZAKV3M.js";
|
|
16
|
+
import {
|
|
17
|
+
allTargets
|
|
18
|
+
} from "./chunk-QVK6VGCV.js";
|
|
19
|
+
import {
|
|
20
|
+
SCANNER_DATA_VERSION,
|
|
21
|
+
SCHEMA_SQL,
|
|
22
|
+
closeDb,
|
|
23
|
+
getDb,
|
|
24
|
+
markResyncComplete,
|
|
25
|
+
needsResync
|
|
26
|
+
} from "./chunk-DZ5HJFB4.js";
|
|
27
|
+
import {
|
|
28
|
+
config
|
|
29
|
+
} from "./chunk-K7YUPLES.js";
|
|
30
|
+
|
|
31
|
+
// src/scanner/reparse.ts
|
|
32
|
+
import fs3 from "fs";
|
|
33
|
+
import { gunzipSync as gunzipSync2 } from "zlib";
|
|
34
|
+
import Database from "better-sqlite3";
|
|
35
|
+
|
|
36
|
+
// src/scanner/loop.ts
|
|
37
|
+
import fs2 from "fs";
|
|
38
|
+
|
|
39
|
+
// src/archive/local.ts
|
|
40
|
+
import fs from "fs";
|
|
41
|
+
import path from "path";
|
|
42
|
+
import { gunzipSync, gzipSync } from "zlib";
|
|
43
|
+
var LocalArchiveBackend = class {
|
|
44
|
+
constructor(baseDir) {
|
|
45
|
+
this.baseDir = baseDir;
|
|
46
|
+
}
|
|
47
|
+
putSync(sessionId, source, content) {
|
|
48
|
+
const dir = path.join(this.baseDir, sessionId);
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
const filePath = path.join(dir, `${source}.jsonl.gz`);
|
|
51
|
+
const compressed = gzipSync(content);
|
|
52
|
+
fs.writeFileSync(filePath, compressed);
|
|
53
|
+
}
|
|
54
|
+
getSync(sessionId, source) {
|
|
55
|
+
const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);
|
|
56
|
+
if (!fs.existsSync(filePath)) return null;
|
|
57
|
+
const compressed = fs.readFileSync(filePath);
|
|
58
|
+
return gunzipSync(compressed);
|
|
59
|
+
}
|
|
60
|
+
hasSync(sessionId, source) {
|
|
61
|
+
const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);
|
|
62
|
+
return fs.existsSync(filePath);
|
|
63
|
+
}
|
|
64
|
+
list() {
|
|
65
|
+
const results = [];
|
|
66
|
+
if (!fs.existsSync(this.baseDir)) return results;
|
|
67
|
+
for (const sessionId of fs.readdirSync(this.baseDir)) {
|
|
68
|
+
const sessionDir = path.join(this.baseDir, sessionId);
|
|
69
|
+
const stat = fs.statSync(sessionDir);
|
|
70
|
+
if (!stat.isDirectory()) continue;
|
|
71
|
+
for (const file of fs.readdirSync(sessionDir)) {
|
|
72
|
+
if (!file.endsWith(".jsonl.gz")) continue;
|
|
73
|
+
const source = file.replace(/\.jsonl\.gz$/, "");
|
|
74
|
+
const fileStat = fs.statSync(path.join(sessionDir, file));
|
|
75
|
+
results.push({
|
|
76
|
+
sessionId,
|
|
77
|
+
source,
|
|
78
|
+
sizeBytes: fileStat.size
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
stats() {
|
|
85
|
+
const entries = this.list();
|
|
86
|
+
return {
|
|
87
|
+
totalFiles: entries.length,
|
|
88
|
+
totalBytes: entries.reduce((sum, e) => sum + e.sizeBytes, 0)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/archive/index.ts
|
|
94
|
+
import path2 from "path";
|
|
95
|
+
var _backend = null;
|
|
96
|
+
function getArchiveBackend() {
|
|
97
|
+
if (!_backend) {
|
|
98
|
+
_backend = new LocalArchiveBackend(path2.join(config.dataDir, "archive"));
|
|
99
|
+
}
|
|
100
|
+
return _backend;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/summary/llm.ts
|
|
104
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
105
|
+
import path3 from "path";
|
|
106
|
+
import { fileURLToPath } from "url";
|
|
107
|
+
|
|
108
|
+
// src/summary/loop.ts
|
|
109
|
+
var MIN_MESSAGES = 3;
|
|
110
|
+
var SUMMARY_THRESHOLD = 20;
|
|
111
|
+
var MAX_PER_CYCLE = 50;
|
|
112
|
+
function buildDeterministicSummary(sessionId) {
|
|
113
|
+
const db = getDb();
|
|
114
|
+
const firstUser = db.prepare(
|
|
115
|
+
"SELECT SUBSTR(content, 1, 200) as content FROM messages WHERE session_id = ? AND role = 'user' AND is_system = 0 ORDER BY ordinal ASC LIMIT 1"
|
|
116
|
+
).get(sessionId);
|
|
117
|
+
const counts = db.prepare(
|
|
118
|
+
"SELECT COUNT(*) as msg_count, SUM(CASE WHEN role = 'user' AND is_system = 0 THEN 1 ELSE 0 END) as user_count FROM messages WHERE session_id = ?"
|
|
119
|
+
).get(sessionId);
|
|
120
|
+
const tools = db.prepare(
|
|
121
|
+
"SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name ORDER BY cnt DESC LIMIT 5"
|
|
122
|
+
).all(sessionId);
|
|
123
|
+
const files = db.prepare(
|
|
124
|
+
"SELECT DISTINCT json_extract(input_json, '$.file_path') as fp FROM tool_calls WHERE session_id = ? AND tool_name IN ('Write', 'Edit') AND input_json IS NOT NULL LIMIT 10"
|
|
125
|
+
).all(sessionId);
|
|
126
|
+
if (!firstUser && counts.msg_count === 0) return null;
|
|
127
|
+
const parts = [];
|
|
128
|
+
if (firstUser) parts.push(`Prompt: "${firstUser.content}"`);
|
|
129
|
+
parts.push(`${counts.msg_count} messages (${counts.user_count} user)`);
|
|
130
|
+
if (tools.length > 0) {
|
|
131
|
+
parts.push(
|
|
132
|
+
`Tools: ${tools.map((t) => `${t.tool_name}(${t.cnt})`).join(", ")}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const filePaths = files.map((f) => f.fp).filter(Boolean);
|
|
136
|
+
if (filePaths.length > 0) {
|
|
137
|
+
parts.push(`Files: ${filePaths.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
return parts.join(". ");
|
|
140
|
+
}
|
|
141
|
+
function generateSummariesOnce(log2 = () => {
|
|
142
|
+
}) {
|
|
143
|
+
const db = getDb();
|
|
144
|
+
let updated = 0;
|
|
145
|
+
const sessions = db.prepare(
|
|
146
|
+
`
|
|
147
|
+
SELECT s.session_id, s.message_count, s.summary_version, s.ended_at_ms,
|
|
148
|
+
EXISTS(SELECT 1 FROM session_repositories WHERE session_id = s.session_id) as has_repo
|
|
149
|
+
FROM sessions s
|
|
150
|
+
WHERE s.message_count >= ?
|
|
151
|
+
AND (
|
|
152
|
+
s.summary IS NULL
|
|
153
|
+
OR (s.message_count - COALESCE(s.summary_version, 0)) >= ?
|
|
154
|
+
OR (s.ended_at_ms IS NOT NULL AND s.ended_at_ms > COALESCE(
|
|
155
|
+
(SELECT MAX(created_at_ms) FROM session_summary_deltas WHERE session_id = s.session_id),
|
|
156
|
+
0
|
|
157
|
+
))
|
|
158
|
+
)
|
|
159
|
+
ORDER BY s.started_at_ms DESC
|
|
160
|
+
LIMIT ?
|
|
161
|
+
`
|
|
162
|
+
).all(MIN_MESSAGES, SUMMARY_THRESHOLD, MAX_PER_CYCLE);
|
|
163
|
+
for (const sess of sessions) {
|
|
164
|
+
try {
|
|
165
|
+
const summary = buildDeterministicSummary(sess.session_id);
|
|
166
|
+
if (!summary) continue;
|
|
167
|
+
db.prepare(
|
|
168
|
+
"UPDATE sessions SET summary = ?, summary_version = ?, sync_dirty = 1, sync_seq = COALESCE(sync_seq, 0) + 1 WHERE session_id = ?"
|
|
169
|
+
).run(summary, sess.message_count, sess.session_id);
|
|
170
|
+
updated++;
|
|
171
|
+
log2(`Summarized ${sess.session_id} (${sess.message_count} messages)`);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
log2(
|
|
174
|
+
`Summary error for ${sess.session_id}: ${err instanceof Error ? err.message : err}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { updated };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/scanner/store.ts
|
|
182
|
+
import path4 from "path";
|
|
183
|
+
function upsertSession2(meta, filePath, source) {
|
|
184
|
+
let project;
|
|
185
|
+
if (meta.cwd) {
|
|
186
|
+
const info = resolveRepoFromCwd(meta.cwd);
|
|
187
|
+
if (info) {
|
|
188
|
+
project = info.repo;
|
|
189
|
+
} else {
|
|
190
|
+
project = path4.basename(meta.cwd);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
upsertSession({
|
|
194
|
+
session_id: meta.sessionId,
|
|
195
|
+
target: source,
|
|
196
|
+
started_at_ms: meta.startedAtMs,
|
|
197
|
+
first_prompt: meta.firstPrompt,
|
|
198
|
+
model: meta.model,
|
|
199
|
+
cli_version: meta.cliVersion,
|
|
200
|
+
scanner_file_path: filePath,
|
|
201
|
+
has_scanner: 1,
|
|
202
|
+
project,
|
|
203
|
+
created_at: meta.startedAtMs ?? Date.now(),
|
|
204
|
+
parent_session_id: meta.parentSessionId,
|
|
205
|
+
relationship_type: meta.relationshipType ?? (meta.parentSessionId ? "subagent" : void 0)
|
|
206
|
+
});
|
|
207
|
+
if (meta.cwd) {
|
|
208
|
+
upsertSessionCwd(meta.sessionId, meta.cwd, meta.startedAtMs ?? Date.now());
|
|
209
|
+
}
|
|
210
|
+
if (meta.cwd) {
|
|
211
|
+
const info = resolveRepoFromCwd(meta.cwd);
|
|
212
|
+
if (info) {
|
|
213
|
+
upsertSessionRepository(
|
|
214
|
+
meta.sessionId,
|
|
215
|
+
info.repo,
|
|
216
|
+
meta.startedAtMs ?? Date.now(),
|
|
217
|
+
void 0,
|
|
218
|
+
info.branch
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var INSERT_TURN_SQL = `
|
|
224
|
+
INSERT OR IGNORE INTO scanner_turns
|
|
225
|
+
(session_id, source, turn_index, timestamp_ms, model, role,
|
|
226
|
+
content_preview, input_tokens, output_tokens,
|
|
227
|
+
cache_read_tokens, cache_creation_tokens, reasoning_tokens)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
229
|
+
`;
|
|
230
|
+
function insertTurns(turns, source) {
|
|
231
|
+
if (turns.length === 0) return;
|
|
232
|
+
const db = getDb();
|
|
233
|
+
const stmt = db.prepare(INSERT_TURN_SQL);
|
|
234
|
+
const tx = db.transaction(() => {
|
|
235
|
+
for (const t of turns) {
|
|
236
|
+
stmt.run(
|
|
237
|
+
t.sessionId,
|
|
238
|
+
source,
|
|
239
|
+
t.turnIndex,
|
|
240
|
+
t.timestampMs,
|
|
241
|
+
t.model ?? null,
|
|
242
|
+
t.role,
|
|
243
|
+
t.contentPreview ?? null,
|
|
244
|
+
t.inputTokens,
|
|
245
|
+
t.outputTokens,
|
|
246
|
+
t.cacheReadTokens,
|
|
247
|
+
t.cacheCreationTokens,
|
|
248
|
+
t.reasoningTokens
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
tx();
|
|
253
|
+
}
|
|
254
|
+
var INSERT_EVENT_SQL = `
|
|
255
|
+
INSERT OR IGNORE INTO scanner_events
|
|
256
|
+
(session_id, source, event_type, timestamp_ms, tool_name, tool_input, tool_output, content, metadata)
|
|
257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
258
|
+
`;
|
|
259
|
+
function insertScannerEvents(events, source) {
|
|
260
|
+
if (events.length === 0) return;
|
|
261
|
+
const db = getDb();
|
|
262
|
+
const stmt = db.prepare(INSERT_EVENT_SQL);
|
|
263
|
+
const tx = db.transaction(() => {
|
|
264
|
+
for (const e of events) {
|
|
265
|
+
stmt.run(
|
|
266
|
+
e.sessionId,
|
|
267
|
+
source,
|
|
268
|
+
e.eventType,
|
|
269
|
+
e.timestampMs,
|
|
270
|
+
e.toolName ?? null,
|
|
271
|
+
e.toolInput ?? null,
|
|
272
|
+
e.toolOutput ?? null,
|
|
273
|
+
e.content ?? null,
|
|
274
|
+
e.metadata ? JSON.stringify(e.metadata) : null
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
tx();
|
|
279
|
+
const seen = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const e of events) {
|
|
281
|
+
if (e.eventType !== "tool_call" || !e.toolInput) continue;
|
|
282
|
+
try {
|
|
283
|
+
const input = JSON.parse(e.toolInput);
|
|
284
|
+
const fp = input.file_path ?? input.path;
|
|
285
|
+
if (typeof fp !== "string" || !path4.isAbsolute(fp)) continue;
|
|
286
|
+
const dir = path4.dirname(fp);
|
|
287
|
+
if (seen.has(dir)) continue;
|
|
288
|
+
seen.add(dir);
|
|
289
|
+
const info = resolveRepoFromCwd(dir);
|
|
290
|
+
if (info) {
|
|
291
|
+
upsertSessionRepository(
|
|
292
|
+
e.sessionId,
|
|
293
|
+
info.repo,
|
|
294
|
+
e.timestampMs,
|
|
295
|
+
void 0,
|
|
296
|
+
info.branch
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
var UPDATE_TOTALS_SQL = `
|
|
304
|
+
UPDATE sessions SET
|
|
305
|
+
total_input_tokens = (SELECT COALESCE(SUM(input_tokens), 0) FROM scanner_turns WHERE session_id = ?),
|
|
306
|
+
total_output_tokens = (SELECT COALESCE(SUM(output_tokens), 0) FROM scanner_turns WHERE session_id = ?),
|
|
307
|
+
total_cache_read_tokens = (SELECT COALESCE(SUM(cache_read_tokens), 0) FROM scanner_turns WHERE session_id = ?),
|
|
308
|
+
total_cache_creation_tokens = (SELECT COALESCE(SUM(cache_creation_tokens), 0) FROM scanner_turns WHERE session_id = ?),
|
|
309
|
+
total_reasoning_tokens = (SELECT COALESCE(SUM(reasoning_tokens), 0) FROM scanner_turns WHERE session_id = ?),
|
|
310
|
+
turn_count = (SELECT COUNT(*) FROM scanner_turns WHERE session_id = ?)
|
|
311
|
+
WHERE session_id = ?
|
|
312
|
+
`;
|
|
313
|
+
function updateSessionTotals(sessionId) {
|
|
314
|
+
const db = getDb();
|
|
315
|
+
db.prepare(UPDATE_TOTALS_SQL).run(
|
|
316
|
+
sessionId,
|
|
317
|
+
sessionId,
|
|
318
|
+
sessionId,
|
|
319
|
+
sessionId,
|
|
320
|
+
sessionId,
|
|
321
|
+
sessionId,
|
|
322
|
+
sessionId
|
|
323
|
+
);
|
|
324
|
+
const toolRows = db.prepare(
|
|
325
|
+
`SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name`
|
|
326
|
+
).all(sessionId);
|
|
327
|
+
const eventRows = db.prepare(
|
|
328
|
+
`SELECT event_type, COUNT(*) as cnt FROM scanner_events WHERE session_id = ? GROUP BY event_type`
|
|
329
|
+
).all(sessionId);
|
|
330
|
+
const toolCounts = {};
|
|
331
|
+
for (const r of toolRows) toolCounts[r.tool_name] = r.cnt;
|
|
332
|
+
const eventCounts = {};
|
|
333
|
+
for (const r of eventRows) {
|
|
334
|
+
const key = r.event_type.startsWith("progress:") ? r.event_type.slice("progress:".length) : r.event_type;
|
|
335
|
+
eventCounts[key] = (eventCounts[key] ?? 0) + r.cnt;
|
|
336
|
+
}
|
|
337
|
+
if (toolRows.length > 0 || eventRows.length > 0) {
|
|
338
|
+
db.prepare(
|
|
339
|
+
`UPDATE sessions
|
|
340
|
+
SET tool_counts = ?,
|
|
341
|
+
event_type_counts = ?,
|
|
342
|
+
sync_seq = COALESCE(sync_seq, 0) + 1
|
|
343
|
+
WHERE session_id = ?`
|
|
344
|
+
).run(JSON.stringify(toolCounts), JSON.stringify(eventCounts), sessionId);
|
|
345
|
+
}
|
|
346
|
+
refreshIfStale().catch(() => {
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function readFileWatermark(filePath) {
|
|
350
|
+
const db = getDb();
|
|
351
|
+
const row = db.prepare(
|
|
352
|
+
"SELECT byte_offset FROM scanner_file_watermarks WHERE file_path = ?"
|
|
353
|
+
).get(filePath);
|
|
354
|
+
return row?.byte_offset ?? 0;
|
|
355
|
+
}
|
|
356
|
+
function writeFileWatermark(filePath, byteOffset) {
|
|
357
|
+
const db = getDb();
|
|
358
|
+
db.prepare(
|
|
359
|
+
`INSERT INTO scanner_file_watermarks (file_path, byte_offset, last_scanned_ms)
|
|
360
|
+
VALUES (?, ?, ?)
|
|
361
|
+
ON CONFLICT(file_path) DO UPDATE SET byte_offset = excluded.byte_offset, last_scanned_ms = excluded.last_scanned_ms`
|
|
362
|
+
).run(filePath, byteOffset, Date.now());
|
|
363
|
+
}
|
|
364
|
+
function snapshotSyncIds(sessionId) {
|
|
365
|
+
const db = getDb();
|
|
366
|
+
const saved = { turns: [], events: [], toolCalls: [] };
|
|
367
|
+
const forkRows = db.prepare(
|
|
368
|
+
"SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'"
|
|
369
|
+
).all(sessionId);
|
|
370
|
+
const allIds = [sessionId, ...forkRows.map((r) => r.session_id)];
|
|
371
|
+
for (const sid of allIds) {
|
|
372
|
+
const turns = db.prepare(
|
|
373
|
+
"SELECT session_id, source, turn_index, sync_id FROM scanner_turns WHERE session_id = ?"
|
|
374
|
+
).all(sid);
|
|
375
|
+
for (const t of turns) {
|
|
376
|
+
saved.turns.push({
|
|
377
|
+
sessionId: t.session_id,
|
|
378
|
+
source: t.source,
|
|
379
|
+
turnIndex: t.turn_index,
|
|
380
|
+
syncId: t.sync_id
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const events = db.prepare(
|
|
384
|
+
"SELECT session_id, source, event_type, timestamp_ms, COALESCE(tool_name, '') as tool_name, sync_id FROM scanner_events WHERE session_id = ?"
|
|
385
|
+
).all(sid);
|
|
386
|
+
for (const e of events) {
|
|
387
|
+
saved.events.push({
|
|
388
|
+
sessionId: e.session_id,
|
|
389
|
+
source: e.source,
|
|
390
|
+
eventType: e.event_type,
|
|
391
|
+
timestampMs: e.timestamp_ms,
|
|
392
|
+
toolName: e.tool_name,
|
|
393
|
+
syncId: e.sync_id
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const tcs = db.prepare(
|
|
397
|
+
`SELECT tc.session_id, m.ordinal, COALESCE(tc.tool_use_id, '') as tool_use_id,
|
|
398
|
+
tc.tool_name, tc.sync_id
|
|
399
|
+
FROM tool_calls tc
|
|
400
|
+
INNER JOIN messages m ON tc.message_id = m.id
|
|
401
|
+
WHERE tc.session_id = ?`
|
|
402
|
+
).all(sid);
|
|
403
|
+
for (const tc of tcs) {
|
|
404
|
+
saved.toolCalls.push({
|
|
405
|
+
sessionId: tc.session_id,
|
|
406
|
+
ordinal: tc.ordinal,
|
|
407
|
+
toolUseId: tc.tool_use_id,
|
|
408
|
+
toolName: tc.tool_name,
|
|
409
|
+
syncId: tc.sync_id
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return saved;
|
|
414
|
+
}
|
|
415
|
+
function resetFileForReparse(filePath, sessionId) {
|
|
416
|
+
const db = getDb();
|
|
417
|
+
const saved = sessionId ? snapshotSyncIds(sessionId) : { turns: [], events: [], toolCalls: [] };
|
|
418
|
+
db.prepare("DELETE FROM scanner_file_watermarks WHERE file_path = ?").run(
|
|
419
|
+
filePath
|
|
420
|
+
);
|
|
421
|
+
if (sessionId) {
|
|
422
|
+
db.prepare("DELETE FROM scanner_turns WHERE session_id = ?").run(sessionId);
|
|
423
|
+
db.prepare("DELETE FROM scanner_events WHERE session_id = ?").run(
|
|
424
|
+
sessionId
|
|
425
|
+
);
|
|
426
|
+
db.prepare("DELETE FROM tool_calls WHERE session_id = ?").run(sessionId);
|
|
427
|
+
db.prepare(
|
|
428
|
+
"DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id = ?)"
|
|
429
|
+
).run(sessionId);
|
|
430
|
+
db.prepare("DELETE FROM messages WHERE session_id = ?").run(sessionId);
|
|
431
|
+
const forkSessionFilter = "SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'";
|
|
432
|
+
db.prepare(
|
|
433
|
+
`DELETE FROM scanner_turns WHERE session_id IN (${forkSessionFilter})`
|
|
434
|
+
).run(sessionId);
|
|
435
|
+
db.prepare(
|
|
436
|
+
`DELETE FROM scanner_events WHERE session_id IN (${forkSessionFilter})`
|
|
437
|
+
).run(sessionId);
|
|
438
|
+
db.prepare(
|
|
439
|
+
`DELETE FROM tool_calls WHERE session_id IN (${forkSessionFilter})`
|
|
440
|
+
).run(sessionId);
|
|
441
|
+
db.prepare(
|
|
442
|
+
`DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id IN (${forkSessionFilter}))`
|
|
443
|
+
).run(sessionId);
|
|
444
|
+
db.prepare(
|
|
445
|
+
`DELETE FROM messages WHERE session_id IN (${forkSessionFilter})`
|
|
446
|
+
).run(sessionId);
|
|
447
|
+
db.prepare(
|
|
448
|
+
"DELETE FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'"
|
|
449
|
+
).run(sessionId);
|
|
450
|
+
}
|
|
451
|
+
return saved;
|
|
452
|
+
}
|
|
453
|
+
function restoreSyncIds(saved) {
|
|
454
|
+
if (!saved.turns.length && !saved.events.length && !saved.toolCalls.length)
|
|
455
|
+
return;
|
|
456
|
+
const db = getDb();
|
|
457
|
+
const tx = db.transaction(() => {
|
|
458
|
+
if (saved.turns.length > 0) {
|
|
459
|
+
const stmt = db.prepare(
|
|
460
|
+
"UPDATE scanner_turns SET sync_id = ? WHERE session_id = ? AND source = ? AND turn_index = ?"
|
|
461
|
+
);
|
|
462
|
+
for (const t of saved.turns) {
|
|
463
|
+
stmt.run(t.syncId, t.sessionId, t.source, t.turnIndex);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (saved.events.length > 0) {
|
|
467
|
+
const stmt = db.prepare(
|
|
468
|
+
`UPDATE scanner_events SET sync_id = ?
|
|
469
|
+
WHERE session_id = ? AND source = ? AND event_type = ? AND timestamp_ms = ?
|
|
470
|
+
AND COALESCE(tool_name, '') = ?`
|
|
471
|
+
);
|
|
472
|
+
for (const e of saved.events) {
|
|
473
|
+
stmt.run(
|
|
474
|
+
e.syncId,
|
|
475
|
+
e.sessionId,
|
|
476
|
+
e.source,
|
|
477
|
+
e.eventType,
|
|
478
|
+
e.timestampMs,
|
|
479
|
+
e.toolName
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (saved.toolCalls.length > 0) {
|
|
484
|
+
const stmt = db.prepare(
|
|
485
|
+
`UPDATE tool_calls SET sync_id = ?
|
|
486
|
+
WHERE message_id IN (SELECT id FROM messages WHERE session_id = ? AND ordinal = ?)
|
|
487
|
+
AND COALESCE(tool_use_id, '') = ?
|
|
488
|
+
AND tool_name = ?`
|
|
489
|
+
);
|
|
490
|
+
for (const tc of saved.toolCalls) {
|
|
491
|
+
stmt.run(
|
|
492
|
+
tc.syncId,
|
|
493
|
+
tc.sessionId,
|
|
494
|
+
tc.ordinal,
|
|
495
|
+
tc.toolUseId,
|
|
496
|
+
tc.toolName
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
tx();
|
|
502
|
+
}
|
|
503
|
+
function getTurnCount(sessionId, source) {
|
|
504
|
+
const db = getDb();
|
|
505
|
+
const row = db.prepare(
|
|
506
|
+
"SELECT COUNT(*) as count FROM scanner_turns WHERE session_id = ? AND source = ?"
|
|
507
|
+
).get(sessionId, source);
|
|
508
|
+
return row.count;
|
|
509
|
+
}
|
|
510
|
+
function readArchivedSize(filePath) {
|
|
511
|
+
const db = getDb();
|
|
512
|
+
const row = db.prepare(
|
|
513
|
+
"SELECT archived_size FROM scanner_file_watermarks WHERE file_path = ?"
|
|
514
|
+
).get(filePath);
|
|
515
|
+
return row?.archived_size ?? 0;
|
|
516
|
+
}
|
|
517
|
+
function writeArchivedSize(filePath, size) {
|
|
518
|
+
const db = getDb();
|
|
519
|
+
db.prepare(
|
|
520
|
+
"UPDATE scanner_file_watermarks SET archived_size = ? WHERE file_path = ?"
|
|
521
|
+
).run(size, filePath);
|
|
522
|
+
}
|
|
523
|
+
function toolUseSummary(toolCalls) {
|
|
524
|
+
return toolCalls.map((tc) => {
|
|
525
|
+
let label = "";
|
|
526
|
+
if (tc.inputJson) {
|
|
527
|
+
try {
|
|
528
|
+
const input = JSON.parse(tc.inputJson);
|
|
529
|
+
label = input.description ?? input.command ?? input.pattern ?? input.file_path ?? input.query ?? input.prompt ?? input.skill ?? "";
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return label ? `[${tc.toolName}: ${label}]` : `[${tc.toolName}]`;
|
|
534
|
+
}).join("\n");
|
|
535
|
+
}
|
|
536
|
+
var INSERT_MESSAGE_SQL = `
|
|
537
|
+
INSERT OR IGNORE INTO messages
|
|
538
|
+
(session_id, ordinal, role, content, timestamp_ms,
|
|
539
|
+
has_thinking, has_tool_use, content_length, is_system,
|
|
540
|
+
model, token_usage, context_tokens, output_tokens,
|
|
541
|
+
has_context_tokens, has_output_tokens, uuid, parent_uuid)
|
|
542
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
543
|
+
`;
|
|
544
|
+
var INSERT_TOOL_CALL_SQL = `
|
|
545
|
+
INSERT INTO tool_calls
|
|
546
|
+
(message_id, session_id, tool_name, category, tool_use_id,
|
|
547
|
+
input_json, skill_name, result_content_length, result_content,
|
|
548
|
+
subagent_session_id, duration_ms)
|
|
549
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
550
|
+
`;
|
|
551
|
+
function insertMessages(messages, orphanedToolResults) {
|
|
552
|
+
if (messages.length === 0 && !orphanedToolResults?.size) return;
|
|
553
|
+
const db = getDb();
|
|
554
|
+
const toolResultMap = /* @__PURE__ */ new Map();
|
|
555
|
+
if (orphanedToolResults) {
|
|
556
|
+
for (const [id, result] of orphanedToolResults) {
|
|
557
|
+
toolResultMap.set(id, result);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const msg of messages) {
|
|
561
|
+
for (const [id, result] of msg.toolResults) {
|
|
562
|
+
toolResultMap.set(id, result);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const msgStmt = db.prepare(INSERT_MESSAGE_SQL);
|
|
566
|
+
const tcStmt = db.prepare(INSERT_TOOL_CALL_SQL);
|
|
567
|
+
const ftsStmt = db.prepare(
|
|
568
|
+
"INSERT INTO messages_fts(rowid, content) VALUES (?, ?)"
|
|
569
|
+
);
|
|
570
|
+
const tx = db.transaction(() => {
|
|
571
|
+
for (const msg of messages) {
|
|
572
|
+
let content = msg.content;
|
|
573
|
+
if (!content && msg.role === "assistant" && msg.toolCalls.length > 0) {
|
|
574
|
+
content = toolUseSummary(msg.toolCalls);
|
|
575
|
+
}
|
|
576
|
+
const result = msgStmt.run(
|
|
577
|
+
msg.sessionId,
|
|
578
|
+
msg.ordinal,
|
|
579
|
+
msg.role,
|
|
580
|
+
content,
|
|
581
|
+
msg.timestampMs ?? null,
|
|
582
|
+
msg.hasThinking ? 1 : 0,
|
|
583
|
+
msg.hasToolUse ? 1 : 0,
|
|
584
|
+
msg.contentLength,
|
|
585
|
+
msg.isSystem ? 1 : 0,
|
|
586
|
+
msg.model ?? "",
|
|
587
|
+
msg.tokenUsage ?? "",
|
|
588
|
+
msg.contextTokens ?? 0,
|
|
589
|
+
msg.outputTokens ?? 0,
|
|
590
|
+
msg.hasContextTokens ? 1 : 0,
|
|
591
|
+
msg.hasOutputTokens ? 1 : 0,
|
|
592
|
+
msg.uuid ?? null,
|
|
593
|
+
msg.parentUuid ?? null
|
|
594
|
+
);
|
|
595
|
+
if (result.changes === 0) continue;
|
|
596
|
+
const messageId = result.lastInsertRowid;
|
|
597
|
+
ftsStmt.run(messageId, content);
|
|
598
|
+
for (const tc of msg.toolCalls) {
|
|
599
|
+
const toolResult = toolResultMap.get(tc.toolUseId);
|
|
600
|
+
const durationMs = tc.timestampMs && toolResult?.timestampMs ? toolResult.timestampMs - tc.timestampMs : null;
|
|
601
|
+
tcStmt.run(
|
|
602
|
+
messageId,
|
|
603
|
+
msg.sessionId,
|
|
604
|
+
tc.toolName,
|
|
605
|
+
tc.category,
|
|
606
|
+
tc.toolUseId,
|
|
607
|
+
tc.inputJson ?? null,
|
|
608
|
+
tc.skillName ?? null,
|
|
609
|
+
toolResult?.contentLength ?? null,
|
|
610
|
+
toolResult?.contentRaw ?? null,
|
|
611
|
+
tc.subagentSessionId ?? null,
|
|
612
|
+
durationMs != null && durationMs >= 0 ? durationMs : null
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (toolResultMap.size > 0) {
|
|
617
|
+
const backfillStmt = db.prepare(
|
|
618
|
+
`UPDATE tool_calls
|
|
619
|
+
SET result_content = ?, result_content_length = ?
|
|
620
|
+
WHERE tool_use_id = ? AND result_content IS NULL`
|
|
621
|
+
);
|
|
622
|
+
for (const [toolUseId, result] of toolResultMap) {
|
|
623
|
+
backfillStmt.run(result.contentRaw, result.contentLength, toolUseId);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
tx();
|
|
628
|
+
}
|
|
629
|
+
function linkSubagentSessions() {
|
|
630
|
+
const db = getDb();
|
|
631
|
+
const result = db.prepare(
|
|
632
|
+
`UPDATE sessions
|
|
633
|
+
SET parent_session_id = (
|
|
634
|
+
SELECT tc.session_id
|
|
635
|
+
FROM tool_calls tc
|
|
636
|
+
WHERE tc.subagent_session_id = sessions.session_id
|
|
637
|
+
LIMIT 1
|
|
638
|
+
),
|
|
639
|
+
relationship_type = 'subagent',
|
|
640
|
+
sync_seq = COALESCE(sync_seq, 0) + 1
|
|
641
|
+
WHERE (relationship_type = '' OR relationship_type IS NULL)
|
|
642
|
+
AND parent_session_id IS NULL
|
|
643
|
+
AND EXISTS (
|
|
644
|
+
SELECT 1 FROM tool_calls tc
|
|
645
|
+
WHERE tc.subagent_session_id = sessions.session_id
|
|
646
|
+
)`
|
|
647
|
+
).run();
|
|
648
|
+
return result.changes;
|
|
649
|
+
}
|
|
650
|
+
function getMaxOrdinal(sessionId) {
|
|
651
|
+
const db = getDb();
|
|
652
|
+
const row = db.prepare(
|
|
653
|
+
"SELECT MAX(ordinal) as max_ord FROM messages WHERE session_id = ?"
|
|
654
|
+
).get(sessionId);
|
|
655
|
+
return row.max_ord ?? -1;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/scanner/loop.ts
|
|
659
|
+
var DEFAULT_IDLE_MS = 6e4;
|
|
660
|
+
var DEFAULT_CATCHUP_MS = 5e3;
|
|
661
|
+
function scanOnce() {
|
|
662
|
+
getDb();
|
|
663
|
+
let filesScanned = 0;
|
|
664
|
+
let newTurns = 0;
|
|
665
|
+
for (const target of allTargets()) {
|
|
666
|
+
if (!target.scanner) continue;
|
|
667
|
+
const source = target.id;
|
|
668
|
+
for (const { filePath } of target.scanner.discover()) {
|
|
669
|
+
let offset = readFileWatermark(filePath);
|
|
670
|
+
let result = target.scanner.parseFile(filePath, offset);
|
|
671
|
+
if (!result) continue;
|
|
672
|
+
let savedSyncIds;
|
|
673
|
+
if (result.needsFullReparse && offset > 0) {
|
|
674
|
+
savedSyncIds = resetFileForReparse(filePath, result.meta?.sessionId);
|
|
675
|
+
offset = 0;
|
|
676
|
+
result = target.scanner.parseFile(filePath, 0);
|
|
677
|
+
if (!result) continue;
|
|
678
|
+
log.scanner.info(`Reparsing ${filePath} from start (fork detected)`);
|
|
679
|
+
}
|
|
680
|
+
filesScanned++;
|
|
681
|
+
if (offset > 0 && result.meta?.sessionId && !result.absoluteIndices) {
|
|
682
|
+
const existingCount = getTurnCount(result.meta.sessionId, source);
|
|
683
|
+
if (existingCount > 0) {
|
|
684
|
+
reindexTurns(result, existingCount);
|
|
685
|
+
}
|
|
686
|
+
if (result.messages.length > 0) {
|
|
687
|
+
const maxOrd = getMaxOrdinal(result.meta.sessionId);
|
|
688
|
+
reindexMessages(result, maxOrd + 1);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!result.meta?.sessionId) {
|
|
692
|
+
writeFileWatermark(filePath, result.newByteOffset);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const sessionId = result.meta.sessionId;
|
|
696
|
+
const fileMeta = result.meta;
|
|
697
|
+
const fileResult = result;
|
|
698
|
+
const db = getDb();
|
|
699
|
+
db.transaction(() => {
|
|
700
|
+
upsertSession2(fileMeta, filePath, source);
|
|
701
|
+
if (fileResult.turns.length > 0) {
|
|
702
|
+
insertTurns(fileResult.turns, source);
|
|
703
|
+
updateSessionTotals(sessionId);
|
|
704
|
+
}
|
|
705
|
+
if (fileResult.events.length > 0) {
|
|
706
|
+
insertScannerEvents(fileResult.events, source);
|
|
707
|
+
}
|
|
708
|
+
if (fileResult.messages.length > 0 || fileResult.orphanedToolResults?.size) {
|
|
709
|
+
insertMessages(fileResult.messages, fileResult.orphanedToolResults);
|
|
710
|
+
updateSessionMessageCounts(sessionId);
|
|
711
|
+
}
|
|
712
|
+
writeFileWatermark(filePath, fileResult.newByteOffset);
|
|
713
|
+
})();
|
|
714
|
+
newTurns += result.turns.length;
|
|
715
|
+
if (result.forks) {
|
|
716
|
+
for (const fork of result.forks) {
|
|
717
|
+
if (!fork.meta?.sessionId) continue;
|
|
718
|
+
const forkSessionId = fork.meta.sessionId;
|
|
719
|
+
const forkMeta = fork.meta;
|
|
720
|
+
db.transaction(() => {
|
|
721
|
+
upsertSession2(forkMeta, filePath, source);
|
|
722
|
+
if (fork.turns.length > 0) {
|
|
723
|
+
insertTurns(fork.turns, source);
|
|
724
|
+
updateSessionTotals(forkSessionId);
|
|
725
|
+
}
|
|
726
|
+
if (fork.events.length > 0) {
|
|
727
|
+
insertScannerEvents(fork.events, source);
|
|
728
|
+
}
|
|
729
|
+
if (fork.messages.length > 0 || fork.orphanedToolResults?.size) {
|
|
730
|
+
insertMessages(fork.messages, fork.orphanedToolResults);
|
|
731
|
+
updateSessionMessageCounts(forkSessionId);
|
|
732
|
+
}
|
|
733
|
+
})();
|
|
734
|
+
newTurns += fork.turns.length;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (savedSyncIds) {
|
|
738
|
+
restoreSyncIds(savedSyncIds);
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const fileSize = fs2.statSync(filePath).size;
|
|
742
|
+
const archivedSize = readArchivedSize(filePath);
|
|
743
|
+
if (fileSize > archivedSize) {
|
|
744
|
+
const rawContent = fs2.readFileSync(filePath);
|
|
745
|
+
getArchiveBackend().putSync(
|
|
746
|
+
result.meta.sessionId,
|
|
747
|
+
source,
|
|
748
|
+
rawContent
|
|
749
|
+
);
|
|
750
|
+
writeArchivedSize(filePath, fileSize);
|
|
751
|
+
}
|
|
752
|
+
} catch (archiveErr) {
|
|
753
|
+
log.scanner.warn(
|
|
754
|
+
`Archive error for ${filePath}: ${archiveErr instanceof Error ? archiveErr.message : archiveErr}`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (filesScanned > 0) {
|
|
760
|
+
const linked = linkSubagentSessions();
|
|
761
|
+
if (linked > 0) {
|
|
762
|
+
log.scanner.info(
|
|
763
|
+
`Linked ${linked} subagent session${linked > 1 ? "s" : ""}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
log.scanner.info(`Scanned ${filesScanned} files, ${newTurns} new turns`);
|
|
767
|
+
}
|
|
768
|
+
return { filesScanned, newTurns };
|
|
769
|
+
}
|
|
770
|
+
function reindexTurns(result, startIndex) {
|
|
771
|
+
for (let i = 0; i < result.turns.length; i++) {
|
|
772
|
+
result.turns[i].turnIndex = startIndex + i;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function reindexMessages(result, startOrdinal) {
|
|
776
|
+
for (let i = 0; i < result.messages.length; i++) {
|
|
777
|
+
result.messages[i].ordinal = startOrdinal + i;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function createScannerLoop(opts) {
|
|
781
|
+
const idleMs = opts.idleIntervalMs ?? DEFAULT_IDLE_MS;
|
|
782
|
+
const catchUpMs = opts.catchUpIntervalMs ?? DEFAULT_CATCHUP_MS;
|
|
783
|
+
let timer = null;
|
|
784
|
+
let stopping = false;
|
|
785
|
+
let reparseChecked = false;
|
|
786
|
+
let ready = false;
|
|
787
|
+
function scheduleNext(hadWork) {
|
|
788
|
+
if (stopping) return;
|
|
789
|
+
const delay = hadWork ? catchUpMs : idleMs;
|
|
790
|
+
timer = setTimeout(() => tick(), delay);
|
|
791
|
+
if (!opts.keepAlive && timer.unref) {
|
|
792
|
+
timer.unref();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function tick() {
|
|
796
|
+
if (stopping) return;
|
|
797
|
+
if (!reparseChecked) {
|
|
798
|
+
reparseChecked = true;
|
|
799
|
+
if (needsResync()) {
|
|
800
|
+
log.scanner.info("Data version outdated \u2014 running atomic reparse...");
|
|
801
|
+
import("./reparse-636YZCE3.js").then(({ reparseAll: reparseAll2 }) => {
|
|
802
|
+
try {
|
|
803
|
+
const result = reparseAll2((msg) => log.scanner.info(msg));
|
|
804
|
+
if (result.success) {
|
|
805
|
+
markResyncComplete();
|
|
806
|
+
} else {
|
|
807
|
+
log.scanner.error(
|
|
808
|
+
`Reparse failed: ${result.error ?? "unknown"}`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
} catch (err) {
|
|
812
|
+
log.scanner.error(
|
|
813
|
+
`Reparse error: ${err instanceof Error ? err.message : err}`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
scheduleNext(true);
|
|
817
|
+
}).catch((err) => {
|
|
818
|
+
log.scanner.error(
|
|
819
|
+
`Reparse import error: ${err instanceof Error ? err.message : err}`
|
|
820
|
+
);
|
|
821
|
+
scheduleNext(false);
|
|
822
|
+
});
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
markResyncComplete();
|
|
826
|
+
}
|
|
827
|
+
let hadWork = false;
|
|
828
|
+
try {
|
|
829
|
+
const { newTurns } = scanOnce();
|
|
830
|
+
hadWork = newTurns > 0;
|
|
831
|
+
if (!ready) {
|
|
832
|
+
ready = true;
|
|
833
|
+
opts.onReady?.();
|
|
834
|
+
}
|
|
835
|
+
if (!hadWork && ready) {
|
|
836
|
+
try {
|
|
837
|
+
generateSummariesOnce((msg) => log.scanner.info(msg));
|
|
838
|
+
} catch (err) {
|
|
839
|
+
log.scanner.error(
|
|
840
|
+
`Session summary error: ${err instanceof Error ? err.message : err}`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
} catch (err) {
|
|
845
|
+
log.scanner.error(
|
|
846
|
+
`Scan error: ${err instanceof Error ? err.message : err}`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (!stopping) {
|
|
850
|
+
scheduleNext(hadWork);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
start() {
|
|
855
|
+
if (timer) return;
|
|
856
|
+
stopping = false;
|
|
857
|
+
log.scanner.info("Starting scanner");
|
|
858
|
+
tick();
|
|
859
|
+
},
|
|
860
|
+
stop() {
|
|
861
|
+
stopping = true;
|
|
862
|
+
if (timer) {
|
|
863
|
+
clearTimeout(timer);
|
|
864
|
+
timer = null;
|
|
865
|
+
log.scanner.info("Stopped scanner");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/scanner/reparse.ts
|
|
872
|
+
var PRESERVED_TABLES = [
|
|
873
|
+
"hook_events",
|
|
874
|
+
"otel_logs",
|
|
875
|
+
"otel_metrics",
|
|
876
|
+
"otel_spans",
|
|
877
|
+
"watermarks",
|
|
878
|
+
"target_session_sync",
|
|
879
|
+
"model_pricing",
|
|
880
|
+
"user_config_snapshots",
|
|
881
|
+
"repo_config_snapshots"
|
|
882
|
+
];
|
|
883
|
+
var SESSION_MERGE_COLUMNS = [
|
|
884
|
+
"has_hooks",
|
|
885
|
+
"has_otel",
|
|
886
|
+
"otel_input_tokens",
|
|
887
|
+
"otel_output_tokens",
|
|
888
|
+
"otel_cache_read_tokens",
|
|
889
|
+
"otel_cache_creation_tokens",
|
|
890
|
+
"summary",
|
|
891
|
+
"summary_version",
|
|
892
|
+
"permission_mode",
|
|
893
|
+
"is_automated",
|
|
894
|
+
"created_at"
|
|
895
|
+
];
|
|
896
|
+
function removeTempFiles(tempPath) {
|
|
897
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
898
|
+
try {
|
|
899
|
+
fs3.unlinkSync(tempPath + suffix);
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function removeWAL(dbPath) {
|
|
905
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
906
|
+
try {
|
|
907
|
+
fs3.unlinkSync(dbPath + suffix);
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function initTempDb(tempPath) {
|
|
913
|
+
const db = new Database(tempPath);
|
|
914
|
+
db.pragma("journal_mode = WAL");
|
|
915
|
+
db.pragma("busy_timeout = 5000");
|
|
916
|
+
db.function(
|
|
917
|
+
"decompress",
|
|
918
|
+
(blob) => blob ? gunzipSync2(blob).toString() : null
|
|
919
|
+
);
|
|
920
|
+
db.exec(SCHEMA_SQL);
|
|
921
|
+
return db;
|
|
922
|
+
}
|
|
923
|
+
function reparseAll(log2 = () => {
|
|
924
|
+
}) {
|
|
925
|
+
const origPath = config.dbPath;
|
|
926
|
+
const tempPath = `${origPath}-reparse`;
|
|
927
|
+
removeTempFiles(tempPath);
|
|
928
|
+
log2("Starting atomic reparse...");
|
|
929
|
+
let tempDb;
|
|
930
|
+
try {
|
|
931
|
+
tempDb = initTempDb(tempPath);
|
|
932
|
+
tempDb.close();
|
|
933
|
+
} catch (err) {
|
|
934
|
+
removeTempFiles(tempPath);
|
|
935
|
+
return {
|
|
936
|
+
success: false,
|
|
937
|
+
filesScanned: 0,
|
|
938
|
+
newTurns: 0,
|
|
939
|
+
error: `Failed to create temp DB: ${err}`
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
let oldSessionCount = 0;
|
|
943
|
+
try {
|
|
944
|
+
const oldDb = new Database(origPath);
|
|
945
|
+
oldSessionCount = oldDb.prepare("SELECT COUNT(*) as c FROM sessions").get().c;
|
|
946
|
+
oldDb.close();
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
closeDb();
|
|
950
|
+
const savedDbPath = config.dbPath;
|
|
951
|
+
config.dbPath = tempPath;
|
|
952
|
+
let filesScanned = 0;
|
|
953
|
+
let newTurns = 0;
|
|
954
|
+
try {
|
|
955
|
+
const result = scanOnce();
|
|
956
|
+
filesScanned = result.filesScanned;
|
|
957
|
+
newTurns = result.newTurns;
|
|
958
|
+
} catch (err) {
|
|
959
|
+
config.dbPath = savedDbPath;
|
|
960
|
+
closeDb();
|
|
961
|
+
getDb();
|
|
962
|
+
removeTempFiles(tempPath);
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
filesScanned: 0,
|
|
966
|
+
newTurns: 0,
|
|
967
|
+
error: `Scan into temp DB failed: ${err}`
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const db = getDb();
|
|
971
|
+
const tempSessionCount = db.prepare("SELECT COUNT(*) as c FROM sessions").get().c;
|
|
972
|
+
closeDb();
|
|
973
|
+
config.dbPath = savedDbPath;
|
|
974
|
+
if (tempSessionCount === 0 && oldSessionCount > 0) {
|
|
975
|
+
log2(
|
|
976
|
+
`Reparse aborted: temp DB has 0 sessions but old DB has ${oldSessionCount}`
|
|
977
|
+
);
|
|
978
|
+
getDb();
|
|
979
|
+
removeTempFiles(tempPath);
|
|
980
|
+
return {
|
|
981
|
+
success: false,
|
|
982
|
+
filesScanned,
|
|
983
|
+
newTurns,
|
|
984
|
+
error: `Aborted: 0 sessions in reparse vs ${oldSessionCount} in old DB`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
log2("Copying preserved data from old database...");
|
|
988
|
+
try {
|
|
989
|
+
tempDb = new Database(tempPath);
|
|
990
|
+
tempDb.pragma("journal_mode = WAL");
|
|
991
|
+
tempDb.function(
|
|
992
|
+
"decompress",
|
|
993
|
+
(blob) => blob ? gunzipSync2(blob).toString() : null
|
|
994
|
+
);
|
|
995
|
+
const escapedPath = origPath.replace(/'/g, "''");
|
|
996
|
+
tempDb.exec(`ATTACH DATABASE '${escapedPath}' AS old_db`);
|
|
997
|
+
const tx = tempDb.transaction(() => {
|
|
998
|
+
for (const table of PRESERVED_TABLES) {
|
|
999
|
+
try {
|
|
1000
|
+
tempDb.exec(
|
|
1001
|
+
`INSERT OR IGNORE INTO main.${table} SELECT * FROM old_db.${table}`
|
|
1002
|
+
);
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
log2(` Skipping ${table}: ${e instanceof Error ? e.message : e}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
tempDb.exec(
|
|
1009
|
+
"INSERT INTO main.hook_events_fts(rowid, payload) SELECT id, decompress(payload) FROM main.hook_events"
|
|
1010
|
+
);
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
log2(` hook_events_fts rebuild: ${e instanceof Error ? e.message : e}`);
|
|
1013
|
+
}
|
|
1014
|
+
const setClauses = SESSION_MERGE_COLUMNS.map(
|
|
1015
|
+
(col) => `${col} = old_db.sessions.${col}`
|
|
1016
|
+
).join(", ");
|
|
1017
|
+
try {
|
|
1018
|
+
tempDb.exec(`
|
|
1019
|
+
UPDATE main.sessions SET ${setClauses}
|
|
1020
|
+
FROM old_db.sessions
|
|
1021
|
+
WHERE main.sessions.session_id = old_db.sessions.session_id
|
|
1022
|
+
`);
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
log2(` Session merge: ${e instanceof Error ? e.message : e}`);
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
tempDb.exec(
|
|
1028
|
+
"INSERT OR IGNORE INTO main.session_repositories SELECT * FROM old_db.session_repositories"
|
|
1029
|
+
);
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
log2(` session_repositories: ${e instanceof Error ? e.message : e}`);
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
tempDb.exec(
|
|
1035
|
+
"INSERT OR IGNORE INTO main.session_cwds SELECT * FROM old_db.session_cwds"
|
|
1036
|
+
);
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
log2(` session_cwds: ${e instanceof Error ? e.message : e}`);
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
tempDb.exec(`
|
|
1042
|
+
UPDATE main.scanner_turns SET sync_id = old_db.scanner_turns.sync_id
|
|
1043
|
+
FROM old_db.scanner_turns
|
|
1044
|
+
WHERE main.scanner_turns.session_id = old_db.scanner_turns.session_id
|
|
1045
|
+
AND main.scanner_turns.source = old_db.scanner_turns.source
|
|
1046
|
+
AND main.scanner_turns.turn_index = old_db.scanner_turns.turn_index
|
|
1047
|
+
`);
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
log2(` scanner_turns sync_id: ${e instanceof Error ? e.message : e}`);
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
tempDb.exec(`
|
|
1053
|
+
UPDATE main.scanner_events SET sync_id = old_db.scanner_events.sync_id
|
|
1054
|
+
FROM old_db.scanner_events
|
|
1055
|
+
WHERE main.scanner_events.session_id = old_db.scanner_events.session_id
|
|
1056
|
+
AND main.scanner_events.source = old_db.scanner_events.source
|
|
1057
|
+
AND main.scanner_events.event_type = old_db.scanner_events.event_type
|
|
1058
|
+
AND main.scanner_events.timestamp_ms = old_db.scanner_events.timestamp_ms
|
|
1059
|
+
AND COALESCE(main.scanner_events.tool_name, '') = COALESCE(old_db.scanner_events.tool_name, '')
|
|
1060
|
+
`);
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
log2(` scanner_events sync_id: ${e instanceof Error ? e.message : e}`);
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
tempDb.exec(`
|
|
1066
|
+
UPDATE main.tool_calls SET sync_id = old_tc.sync_id
|
|
1067
|
+
FROM old_db.tool_calls old_tc
|
|
1068
|
+
INNER JOIN old_db.messages old_m ON old_tc.message_id = old_m.id
|
|
1069
|
+
INNER JOIN main.messages new_m ON new_m.session_id = old_m.session_id AND new_m.ordinal = old_m.ordinal
|
|
1070
|
+
WHERE main.tool_calls.message_id = new_m.id
|
|
1071
|
+
AND COALESCE(main.tool_calls.tool_use_id, '') = COALESCE(old_tc.tool_use_id, '')
|
|
1072
|
+
AND main.tool_calls.tool_name = old_tc.tool_name
|
|
1073
|
+
`);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
log2(` tool_calls sync_id: ${e instanceof Error ? e.message : e}`);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
tx();
|
|
1079
|
+
tempDb.exec("DETACH DATABASE old_db");
|
|
1080
|
+
tempDb.pragma(`user_version = ${SCANNER_DATA_VERSION}`);
|
|
1081
|
+
tempDb.close();
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
log2(`Failed to copy preserved data: ${err}`);
|
|
1084
|
+
getDb();
|
|
1085
|
+
removeTempFiles(tempPath);
|
|
1086
|
+
return {
|
|
1087
|
+
success: false,
|
|
1088
|
+
filesScanned,
|
|
1089
|
+
newTurns,
|
|
1090
|
+
error: `Copy preserved data failed: ${err}`
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
log2("Swapping database files...");
|
|
1094
|
+
try {
|
|
1095
|
+
removeWAL(origPath);
|
|
1096
|
+
fs3.renameSync(tempPath, origPath);
|
|
1097
|
+
removeWAL(tempPath);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
log2(`File swap failed: ${err}`);
|
|
1100
|
+
getDb();
|
|
1101
|
+
removeTempFiles(tempPath);
|
|
1102
|
+
return {
|
|
1103
|
+
success: false,
|
|
1104
|
+
filesScanned,
|
|
1105
|
+
newTurns,
|
|
1106
|
+
error: `Atomic swap failed: ${err}`
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
getDb();
|
|
1110
|
+
log2(
|
|
1111
|
+
`Reparse complete: ${filesScanned} files, ${newTurns} turns, ${tempSessionCount} sessions`
|
|
1112
|
+
);
|
|
1113
|
+
return { success: true, filesScanned, newTurns };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export {
|
|
1117
|
+
reparseAll,
|
|
1118
|
+
createScannerLoop
|
|
1119
|
+
};
|
|
1120
|
+
//# sourceMappingURL=chunk-NXH7AONS.js.map
|