@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.
Files changed (124) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +5 -0
  3. package/README.md +363 -0
  4. package/bin/hook-handler +3 -0
  5. package/bin/mcp-server +3 -0
  6. package/bin/panopticon +3 -0
  7. package/bin/proxy +3 -0
  8. package/bin/server +3 -0
  9. package/dist/api/client.d.ts +67 -0
  10. package/dist/api/client.js +48 -0
  11. package/dist/api/client.js.map +1 -0
  12. package/dist/chunk-3BUJ7URA.js +387 -0
  13. package/dist/chunk-3BUJ7URA.js.map +1 -0
  14. package/dist/chunk-3TZAKV3M.js +158 -0
  15. package/dist/chunk-3TZAKV3M.js.map +1 -0
  16. package/dist/chunk-4SM2H22C.js +169 -0
  17. package/dist/chunk-4SM2H22C.js.map +1 -0
  18. package/dist/chunk-7Q3BJMLG.js +62 -0
  19. package/dist/chunk-7Q3BJMLG.js.map +1 -0
  20. package/dist/chunk-BVOE7A2Z.js +412 -0
  21. package/dist/chunk-BVOE7A2Z.js.map +1 -0
  22. package/dist/chunk-CF4GPWLI.js +170 -0
  23. package/dist/chunk-CF4GPWLI.js.map +1 -0
  24. package/dist/chunk-DZ5HJFB4.js +467 -0
  25. package/dist/chunk-DZ5HJFB4.js.map +1 -0
  26. package/dist/chunk-HQCY722C.js +428 -0
  27. package/dist/chunk-HQCY722C.js.map +1 -0
  28. package/dist/chunk-HRCEIYKU.js +134 -0
  29. package/dist/chunk-HRCEIYKU.js.map +1 -0
  30. package/dist/chunk-K7YUPLES.js +76 -0
  31. package/dist/chunk-K7YUPLES.js.map +1 -0
  32. package/dist/chunk-L7G27XWF.js +130 -0
  33. package/dist/chunk-L7G27XWF.js.map +1 -0
  34. package/dist/chunk-LWXF7YRG.js +626 -0
  35. package/dist/chunk-LWXF7YRG.js.map +1 -0
  36. package/dist/chunk-NXH7AONS.js +1120 -0
  37. package/dist/chunk-NXH7AONS.js.map +1 -0
  38. package/dist/chunk-QK5442ZP.js +55 -0
  39. package/dist/chunk-QK5442ZP.js.map +1 -0
  40. package/dist/chunk-QVK6VGCV.js +1703 -0
  41. package/dist/chunk-QVK6VGCV.js.map +1 -0
  42. package/dist/chunk-RX2RXHBH.js +1699 -0
  43. package/dist/chunk-RX2RXHBH.js.map +1 -0
  44. package/dist/chunk-SEXU2WYG.js +788 -0
  45. package/dist/chunk-SEXU2WYG.js.map +1 -0
  46. package/dist/chunk-SUGSQ4YI.js +264 -0
  47. package/dist/chunk-SUGSQ4YI.js.map +1 -0
  48. package/dist/chunk-TGXFVAID.js +138 -0
  49. package/dist/chunk-TGXFVAID.js.map +1 -0
  50. package/dist/chunk-WLBNFVIG.js +447 -0
  51. package/dist/chunk-WLBNFVIG.js.map +1 -0
  52. package/dist/chunk-XLTCUH5A.js +1072 -0
  53. package/dist/chunk-XLTCUH5A.js.map +1 -0
  54. package/dist/chunk-YVRWVDIA.js +146 -0
  55. package/dist/chunk-YVRWVDIA.js.map +1 -0
  56. package/dist/chunk-ZEC4LRKS.js +176 -0
  57. package/dist/chunk-ZEC4LRKS.js.map +1 -0
  58. package/dist/cli.d.ts +1 -0
  59. package/dist/cli.js +1084 -0
  60. package/dist/cli.js.map +1 -0
  61. package/dist/config-NwoZC-GM.d.ts +20 -0
  62. package/dist/db.d.ts +46 -0
  63. package/dist/db.js +15 -0
  64. package/dist/db.js.map +1 -0
  65. package/dist/doctor.d.ts +37 -0
  66. package/dist/doctor.js +14 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/hooks/handler.d.ts +23 -0
  69. package/dist/hooks/handler.js +295 -0
  70. package/dist/hooks/handler.js.map +1 -0
  71. package/dist/index.d.ts +57 -0
  72. package/dist/index.js +101 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/mcp/server.d.ts +1 -0
  75. package/dist/mcp/server.js +243 -0
  76. package/dist/mcp/server.js.map +1 -0
  77. package/dist/otlp/server.d.ts +7 -0
  78. package/dist/otlp/server.js +17 -0
  79. package/dist/otlp/server.js.map +1 -0
  80. package/dist/permissions.d.ts +33 -0
  81. package/dist/permissions.js +14 -0
  82. package/dist/permissions.js.map +1 -0
  83. package/dist/pricing.d.ts +29 -0
  84. package/dist/pricing.js +13 -0
  85. package/dist/pricing.js.map +1 -0
  86. package/dist/proxy/server.d.ts +10 -0
  87. package/dist/proxy/server.js +20 -0
  88. package/dist/proxy/server.js.map +1 -0
  89. package/dist/prune.d.ts +18 -0
  90. package/dist/prune.js +13 -0
  91. package/dist/prune.js.map +1 -0
  92. package/dist/query.d.ts +56 -0
  93. package/dist/query.js +27 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/reparse-636YZCE3.js +14 -0
  96. package/dist/reparse-636YZCE3.js.map +1 -0
  97. package/dist/repo.d.ts +17 -0
  98. package/dist/repo.js +9 -0
  99. package/dist/repo.js.map +1 -0
  100. package/dist/scanner.d.ts +73 -0
  101. package/dist/scanner.js +15 -0
  102. package/dist/scanner.js.map +1 -0
  103. package/dist/sdk.d.ts +82 -0
  104. package/dist/sdk.js +208 -0
  105. package/dist/sdk.js.map +1 -0
  106. package/dist/server.d.ts +5 -0
  107. package/dist/server.js +25 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/setup.d.ts +35 -0
  110. package/dist/setup.js +19 -0
  111. package/dist/setup.js.map +1 -0
  112. package/dist/sync/index.d.ts +29 -0
  113. package/dist/sync/index.js +32 -0
  114. package/dist/sync/index.js.map +1 -0
  115. package/dist/targets.d.ts +279 -0
  116. package/dist/targets.js +20 -0
  117. package/dist/targets.js.map +1 -0
  118. package/dist/types-D-MYCBol.d.ts +128 -0
  119. package/dist/types.d.ts +164 -0
  120. package/dist/types.js +1 -0
  121. package/dist/types.js.map +1 -0
  122. package/hooks/hooks.json +274 -0
  123. package/package.json +124 -0
  124. package/skills/panopticon-optimize/SKILL.md +222 -0
@@ -0,0 +1,428 @@
1
+ import {
2
+ captureException
3
+ } from "./chunk-CF4GPWLI.js";
4
+ import {
5
+ log
6
+ } from "./chunk-7Q3BJMLG.js";
7
+ import {
8
+ SESSION_READERS,
9
+ TABLE_SYNC_REGISTRY,
10
+ readWatermark,
11
+ watermarkKey,
12
+ writeWatermark
13
+ } from "./chunk-SEXU2WYG.js";
14
+ import {
15
+ loadUnifiedConfig,
16
+ saveUnifiedConfig
17
+ } from "./chunk-QK5442ZP.js";
18
+ import {
19
+ getDb
20
+ } from "./chunk-DZ5HJFB4.js";
21
+
22
+ // src/sync/config.ts
23
+ function loadSyncConfig() {
24
+ const cfg = loadUnifiedConfig();
25
+ return { targets: cfg.sync.targets, filter: cfg.sync.filter };
26
+ }
27
+ function saveSyncConfig(syncCfg) {
28
+ const cfg = loadUnifiedConfig();
29
+ cfg.sync.targets = syncCfg.targets;
30
+ cfg.sync.filter = syncCfg.filter;
31
+ saveUnifiedConfig(cfg);
32
+ }
33
+ function addTarget(target) {
34
+ const cfg = loadUnifiedConfig();
35
+ const existing = cfg.sync.targets.findIndex((t) => t.name === target.name);
36
+ if (existing >= 0) {
37
+ cfg.sync.targets[existing] = target;
38
+ } else {
39
+ cfg.sync.targets.push(target);
40
+ }
41
+ saveUnifiedConfig(cfg);
42
+ }
43
+ function removeTarget(name) {
44
+ const cfg = loadUnifiedConfig();
45
+ const before = cfg.sync.targets.length;
46
+ cfg.sync.targets = cfg.sync.targets.filter((t) => t.name !== name);
47
+ if (cfg.sync.targets.length === before) return false;
48
+ saveUnifiedConfig(cfg);
49
+ return true;
50
+ }
51
+ function listTargets() {
52
+ return loadUnifiedConfig().sync.targets;
53
+ }
54
+
55
+ // src/sync/loop.ts
56
+ import { execSync } from "child_process";
57
+
58
+ // src/sync/post.ts
59
+ var MAX_RETRIES = 5;
60
+ var BASE_DELAY_MS = 2e3;
61
+ var REQUEST_TIMEOUT_MS = 3e4;
62
+ async function postSync(url, body, headers) {
63
+ let lastError = null;
64
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
65
+ try {
66
+ const controller = new AbortController();
67
+ const timeoutId = setTimeout(
68
+ () => controller.abort(),
69
+ REQUEST_TIMEOUT_MS
70
+ );
71
+ const response = await fetch(url, {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json", ...headers },
74
+ body: JSON.stringify(body),
75
+ signal: controller.signal
76
+ });
77
+ clearTimeout(timeoutId);
78
+ if (response.ok) {
79
+ const text = await response.text().catch(() => "");
80
+ try {
81
+ return text ? JSON.parse(text) : {};
82
+ } catch {
83
+ return {};
84
+ }
85
+ }
86
+ const status = response.status;
87
+ if (status >= 400 && status < 500 && status !== 429) {
88
+ const text = await response.text().catch(() => "");
89
+ throw new Error(`HTTP ${status}: ${text}`);
90
+ }
91
+ lastError = new Error(`HTTP ${status}`);
92
+ } catch (err) {
93
+ if (err instanceof Error && err.message.startsWith("HTTP 4") && !err.message.startsWith("HTTP 429")) {
94
+ throw err;
95
+ }
96
+ lastError = err instanceof Error ? err : new Error(String(err));
97
+ }
98
+ if (attempt < MAX_RETRIES) {
99
+ const delay = BASE_DELAY_MS * 2 ** attempt;
100
+ await new Promise((r) => setTimeout(r, delay));
101
+ }
102
+ }
103
+ throw lastError ?? new Error("postSync failed");
104
+ }
105
+
106
+ // src/sync/loop.ts
107
+ var DEFAULT_BATCH_SIZE = 2e3;
108
+ var DEFAULT_POST_BATCH_SIZE = 100;
109
+ var DEFAULT_IDLE_MS = 3e4;
110
+ var DEFAULT_CATCHUP_MS = 100;
111
+ var MAX_SESSIONS_PER_TICK = 10;
112
+ var WM_COLUMNS = {
113
+ messages: "wm_messages",
114
+ tool_calls: "wm_tool_calls",
115
+ scanner_turns: "wm_scanner_turns",
116
+ scanner_events: "wm_scanner_events",
117
+ hook_events: "wm_hook_events",
118
+ otel_logs: "wm_otel_logs",
119
+ otel_metrics: "wm_otel_metrics",
120
+ otel_spans: "wm_otel_spans"
121
+ };
122
+ function matchesGlob(value, pattern) {
123
+ if (pattern === "*/*") return true;
124
+ if (pattern.endsWith("/*")) {
125
+ return value.startsWith(pattern.slice(0, -1));
126
+ }
127
+ return value === pattern;
128
+ }
129
+ function repoMatchesFilter(repository, filter) {
130
+ if (!filter) return true;
131
+ if (filter.excludeRepos?.some((p) => matchesGlob(repository, p)))
132
+ return false;
133
+ if (filter.includeRepos?.length) {
134
+ if (!filter.includeRepos.some((p) => matchesGlob(repository, p)))
135
+ return false;
136
+ }
137
+ return true;
138
+ }
139
+ function buildSyncableSessionIds(opts) {
140
+ const requireRepo = opts.filter?.requireRepo ?? true;
141
+ if (!requireRepo && !opts.filter?.includeRepos?.length) return null;
142
+ const db = getDb();
143
+ const rows = db.prepare(
144
+ "SELECT DISTINCT sr.session_id, sr.repository FROM session_repositories sr"
145
+ ).all();
146
+ const sessionIds = /* @__PURE__ */ new Set();
147
+ for (const row of rows) {
148
+ if (!repoMatchesFilter(row.repository, opts.filter)) continue;
149
+ sessionIds.add(row.session_id);
150
+ }
151
+ return sessionIds;
152
+ }
153
+ var TOKEN_CACHE_TTL_MS = 5 * 60 * 1e3;
154
+ var tokenCache = /* @__PURE__ */ new Map();
155
+ function resolveToken(target) {
156
+ if (target.token) return target.token;
157
+ if (!target.tokenCommand) return void 0;
158
+ const cached = tokenCache.get(target.name);
159
+ if (cached && Date.now() < cached.expiresAt) return cached.token;
160
+ try {
161
+ const token = execSync(target.tokenCommand, {
162
+ encoding: "utf-8",
163
+ timeout: 1e4,
164
+ stdio: ["ignore", "pipe", "ignore"]
165
+ }).trim();
166
+ if (token) {
167
+ tokenCache.set(target.name, {
168
+ token,
169
+ expiresAt: Date.now() + TOKEN_CACHE_TTL_MS
170
+ });
171
+ }
172
+ return token || void 0;
173
+ } catch (err) {
174
+ log.sync.error(
175
+ `tokenCommand failed for "${target.name}": ${err instanceof Error ? err.message : err}`
176
+ );
177
+ return void 0;
178
+ }
179
+ }
180
+ function createSyncLoop(opts) {
181
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
182
+ const postBatchSize = opts.postBatchSize ?? DEFAULT_POST_BATCH_SIZE;
183
+ const idleMs = opts.idleIntervalMs ?? DEFAULT_IDLE_MS;
184
+ const catchUpMs = opts.catchUpIntervalMs ?? DEFAULT_CATCHUP_MS;
185
+ const panopticonVersion = true ? "0.1.0+2aee981" : "dev";
186
+ function resolveHeaders(target) {
187
+ const headers = { ...target.headers };
188
+ headers["X-Panopticon-Version"] = panopticonVersion;
189
+ const token = resolveToken(target);
190
+ if (token) {
191
+ headers.Authorization = `Bearer ${token}`;
192
+ }
193
+ return headers;
194
+ }
195
+ let timer = null;
196
+ let syncing = false;
197
+ let stopping = false;
198
+ function scheduleNext(hadWork) {
199
+ if (stopping) return;
200
+ const delay = hadWork ? catchUpMs : idleMs;
201
+ timer = setTimeout(() => {
202
+ tick().catch((err) => log.sync.error(`Tick error: ${err}`));
203
+ }, delay);
204
+ if (!opts.keepAlive && timer.unref) {
205
+ timer.unref();
206
+ }
207
+ }
208
+ async function syncSessions(target, syncableSessionIds) {
209
+ const sessionsDesc = TABLE_SYNC_REGISTRY.find(
210
+ (d) => d.table === "sessions"
211
+ );
212
+ if (!sessionsDesc) return false;
213
+ const wmKey = watermarkKey(sessionsDesc.table, target.name);
214
+ const wm = readWatermark(wmKey);
215
+ const { rows, maxId } = sessionsDesc.read(wm, batchSize);
216
+ if (rows.length === 0) return false;
217
+ let filtered = rows;
218
+ if (syncableSessionIds) {
219
+ filtered = rows.filter((r) => {
220
+ const row = r;
221
+ const sessionId = row.sessionId ?? row.session_id;
222
+ return sessionId && syncableSessionIds.has(sessionId);
223
+ });
224
+ }
225
+ if (filtered.length > 0) {
226
+ log.sync.info(
227
+ `sessions: ${filtered.length} sessions (watermark ${wm} \u2192 ${maxId})`
228
+ );
229
+ for (let j = 0; j < filtered.length; j += postBatchSize) {
230
+ const batch = filtered.slice(j, j + postBatchSize);
231
+ const response = await postSync(
232
+ `${target.url}/v1/sync`,
233
+ { table: "sessions", rows: batch },
234
+ resolveHeaders(target)
235
+ );
236
+ const accepted = response.accepted;
237
+ if (Array.isArray(accepted)) {
238
+ recordConfirmedSessions(accepted, target.name);
239
+ }
240
+ }
241
+ }
242
+ writeWatermark(wmKey, maxId);
243
+ return rows.length === batchSize;
244
+ }
245
+ async function syncSessionData(target) {
246
+ const db = getDb();
247
+ db.prepare(
248
+ `DELETE FROM target_session_sync
249
+ WHERE session_id NOT IN (SELECT session_id FROM sessions)`
250
+ ).run();
251
+ const pending = db.prepare(
252
+ `SELECT session_id, sync_seq,
253
+ wm_messages, wm_tool_calls, wm_scanner_turns,
254
+ wm_scanner_events, wm_hook_events, wm_otel_logs,
255
+ wm_otel_metrics, wm_otel_spans
256
+ FROM target_session_sync
257
+ WHERE target = ? AND confirmed = 1
258
+ AND sync_seq > synced_seq
259
+ ORDER BY rowid
260
+ LIMIT ?`
261
+ ).all(target.name, MAX_SESSIONS_PER_TICK);
262
+ if (pending.length === 0) return false;
263
+ const headers = resolveHeaders(target);
264
+ const url = `${target.url}/v1/sync`;
265
+ for (const entry of pending) {
266
+ const wmRow = entry;
267
+ const watermarks = {};
268
+ for (const [table, col] of Object.entries(WM_COLUMNS)) {
269
+ watermarks[table] = wmRow[col] ?? 0;
270
+ }
271
+ let anyData = false;
272
+ for (const [table, reader] of Object.entries(SESSION_READERS)) {
273
+ let afterId = watermarks[table] ?? 0;
274
+ for (; ; ) {
275
+ const { rows, maxId } = reader(entry.session_id, afterId, batchSize);
276
+ if (rows.length === 0) break;
277
+ anyData = true;
278
+ for (let i = 0; i < rows.length; i += postBatchSize) {
279
+ const batch = rows.slice(i, i + postBatchSize);
280
+ await postSync(url, { table, rows: batch }, headers);
281
+ }
282
+ afterId = maxId;
283
+ watermarks[table] = maxId;
284
+ if (rows.length < batchSize) break;
285
+ }
286
+ }
287
+ db.prepare(
288
+ `UPDATE target_session_sync
289
+ SET wm_messages = ?, wm_tool_calls = ?, wm_scanner_turns = ?,
290
+ wm_scanner_events = ?, wm_hook_events = ?, wm_otel_logs = ?,
291
+ wm_otel_metrics = ?, wm_otel_spans = ?, synced_seq = ?
292
+ WHERE session_id = ? AND target = ?`
293
+ ).run(
294
+ watermarks.messages,
295
+ watermarks.tool_calls,
296
+ watermarks.scanner_turns,
297
+ watermarks.scanner_events,
298
+ watermarks.hook_events,
299
+ watermarks.otel_logs,
300
+ watermarks.otel_metrics,
301
+ watermarks.otel_spans,
302
+ entry.sync_seq,
303
+ entry.session_id,
304
+ target.name
305
+ );
306
+ if (anyData) {
307
+ log.sync.info(
308
+ `session-sync: synced data for ${entry.session_id} to ${target.name}`
309
+ );
310
+ }
311
+ }
312
+ const remaining = db.prepare(
313
+ `SELECT COUNT(*) as cnt FROM target_session_sync
314
+ WHERE target = ? AND confirmed = 1
315
+ AND sync_seq > synced_seq`
316
+ ).get(target.name);
317
+ return remaining.cnt > 0;
318
+ }
319
+ async function syncNonSessionTables(target) {
320
+ let hasMore = false;
321
+ for (const desc of TABLE_SYNC_REGISTRY) {
322
+ if (desc.sessionLinked) continue;
323
+ const wmKey = watermarkKey(desc.table, target.name);
324
+ const wm = readWatermark(wmKey);
325
+ const { rows, maxId } = desc.read(wm, batchSize);
326
+ if (rows.length === 0) continue;
327
+ const filtered = desc.table === "repo_config_snapshots" ? rows.filter((r) => {
328
+ const row = r;
329
+ return repoMatchesFilter(row.repository, opts.filter);
330
+ }) : rows;
331
+ if (filtered.length > 0) {
332
+ log.sync.info(
333
+ `${desc.table}: ${filtered.length} ${desc.logNoun} (watermark ${wm} \u2192 ${maxId})`
334
+ );
335
+ for (let j = 0; j < filtered.length; j += postBatchSize) {
336
+ const batch = filtered.slice(j, j + postBatchSize);
337
+ await postSync(
338
+ `${target.url}/v1/sync`,
339
+ { table: desc.table, rows: batch },
340
+ resolveHeaders(target)
341
+ );
342
+ }
343
+ }
344
+ writeWatermark(wmKey, maxId);
345
+ if (rows.length === batchSize) hasMore = true;
346
+ }
347
+ return hasMore;
348
+ }
349
+ function recordConfirmedSessions(sessionIds, targetName) {
350
+ const db = getDb();
351
+ const upsert = db.prepare(
352
+ `INSERT INTO target_session_sync (session_id, target, confirmed, sync_seq)
353
+ VALUES (?, ?, 1, (SELECT sync_seq FROM sessions WHERE session_id = ?))
354
+ ON CONFLICT(session_id, target) DO UPDATE SET
355
+ confirmed = 1,
356
+ sync_seq = MAX(target_session_sync.sync_seq,
357
+ (SELECT sync_seq FROM sessions WHERE session_id = excluded.session_id))`
358
+ );
359
+ for (const sessionId of sessionIds) {
360
+ upsert.run(sessionId, targetName, sessionId);
361
+ }
362
+ }
363
+ async function runOnce() {
364
+ let hasMore = false;
365
+ const syncableSessionIds = buildSyncableSessionIds(opts);
366
+ for (const target of opts.targets) {
367
+ try {
368
+ if (await syncSessions(target, syncableSessionIds)) hasMore = true;
369
+ if (await syncSessionData(target)) hasMore = true;
370
+ if (await syncNonSessionTables(target)) hasMore = true;
371
+ } catch (err) {
372
+ log.sync.error(
373
+ `Error syncing to ${target.name}: ${err instanceof Error ? err.message : err}`
374
+ );
375
+ captureException(err, {
376
+ component: "sync",
377
+ target: target.name
378
+ });
379
+ }
380
+ }
381
+ return hasMore;
382
+ }
383
+ async function tick() {
384
+ if (syncing || stopping) return;
385
+ syncing = true;
386
+ let hasMore = false;
387
+ try {
388
+ getDb();
389
+ hasMore = await runOnce();
390
+ } catch (err) {
391
+ log.sync.error(
392
+ `Cycle error: ${err instanceof Error ? err.stack ?? err.message : err}`
393
+ );
394
+ captureException(err, { component: "sync" });
395
+ } finally {
396
+ syncing = false;
397
+ }
398
+ if (!stopping) {
399
+ scheduleNext(hasMore);
400
+ }
401
+ }
402
+ return {
403
+ start() {
404
+ if (timer || syncing) return;
405
+ stopping = false;
406
+ log.sync.info("Starting sync");
407
+ tick().catch((err) => log.sync.error(`Tick error: ${err}`));
408
+ },
409
+ stop() {
410
+ stopping = true;
411
+ if (timer) {
412
+ clearTimeout(timer);
413
+ timer = null;
414
+ log.sync.info("Stopped sync");
415
+ }
416
+ }
417
+ };
418
+ }
419
+
420
+ export {
421
+ loadSyncConfig,
422
+ saveSyncConfig,
423
+ addTarget,
424
+ removeTarget,
425
+ listTargets,
426
+ createSyncLoop
427
+ };
428
+ //# sourceMappingURL=chunk-HQCY722C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sync/config.ts","../src/sync/loop.ts","../src/sync/post.ts"],"sourcesContent":["import { loadUnifiedConfig, saveUnifiedConfig } from \"../unified-config.js\";\nimport type { SyncFilter, SyncTarget } from \"./types.js\";\n\nexport interface SyncConfig {\n targets: SyncTarget[];\n filter?: SyncFilter;\n}\n\nexport function loadSyncConfig(): SyncConfig {\n const cfg = loadUnifiedConfig();\n return { targets: cfg.sync.targets, filter: cfg.sync.filter };\n}\n\nexport function saveSyncConfig(syncCfg: SyncConfig): void {\n const cfg = loadUnifiedConfig();\n cfg.sync.targets = syncCfg.targets;\n cfg.sync.filter = syncCfg.filter;\n saveUnifiedConfig(cfg);\n}\n\nexport function addTarget(target: SyncTarget): void {\n const cfg = loadUnifiedConfig();\n const existing = cfg.sync.targets.findIndex((t) => t.name === target.name);\n if (existing >= 0) {\n cfg.sync.targets[existing] = target;\n } else {\n cfg.sync.targets.push(target);\n }\n saveUnifiedConfig(cfg);\n}\n\nexport function removeTarget(name: string): boolean {\n const cfg = loadUnifiedConfig();\n const before = cfg.sync.targets.length;\n cfg.sync.targets = cfg.sync.targets.filter((t) => t.name !== name);\n if (cfg.sync.targets.length === before) return false;\n saveUnifiedConfig(cfg);\n return true;\n}\n\nexport function listTargets(): SyncTarget[] {\n return loadUnifiedConfig().sync.targets;\n}\n","declare const __PANOPTICON_VERSION__: string;\n\nimport { execSync } from \"node:child_process\";\nimport { getDb } from \"../db/schema.js\";\nimport { log } from \"../log.js\";\nimport { captureException } from \"../sentry.js\";\nimport { postSync } from \"./post.js\";\nimport { SESSION_READERS } from \"./reader.js\";\nimport { TABLE_SYNC_REGISTRY } from \"./registry.js\";\nimport type {\n SyncFilter,\n SyncHandle,\n SyncOptions,\n SyncTarget,\n} from \"./types.js\";\nimport { readWatermark, watermarkKey, writeWatermark } from \"./watermark.js\";\n\nconst DEFAULT_BATCH_SIZE = 2000;\nconst DEFAULT_POST_BATCH_SIZE = 100;\nconst DEFAULT_IDLE_MS = 30_000;\nconst DEFAULT_CATCHUP_MS = 100;\nconst MAX_SESSIONS_PER_TICK = 10;\n\n/** Maps SESSION_READERS table name → target_session_sync column name. */\nconst WM_COLUMNS = {\n messages: \"wm_messages\",\n tool_calls: \"wm_tool_calls\",\n scanner_turns: \"wm_scanner_turns\",\n scanner_events: \"wm_scanner_events\",\n hook_events: \"wm_hook_events\",\n otel_logs: \"wm_otel_logs\",\n otel_metrics: \"wm_otel_metrics\",\n otel_spans: \"wm_otel_spans\",\n} as const;\n\nfunction matchesGlob(value: string, pattern: string): boolean {\n if (pattern === \"*/*\") return true;\n if (pattern.endsWith(\"/*\")) {\n return value.startsWith(pattern.slice(0, -1));\n }\n return value === pattern;\n}\n\n/** Returns true if the repository passes the include/exclude filter. */\nfunction repoMatchesFilter(repository: string, filter?: SyncFilter): boolean {\n if (!filter) return true;\n if (filter.excludeRepos?.some((p) => matchesGlob(repository, p)))\n return false;\n if (filter.includeRepos?.length) {\n if (!filter.includeRepos.some((p) => matchesGlob(repository, p)))\n return false;\n }\n return true;\n}\n\n/** Set of session IDs that have repo attribution matching the filter. */\nfunction buildSyncableSessionIds(opts: SyncOptions): Set<string> | null {\n const requireRepo = opts.filter?.requireRepo ?? true;\n if (!requireRepo && !opts.filter?.includeRepos?.length) return null; // no filtering\n\n const db = getDb();\n const rows = db\n .prepare(\n \"SELECT DISTINCT sr.session_id, sr.repository FROM session_repositories sr\",\n )\n .all() as Array<{ session_id: string; repository: string }>;\n\n const sessionIds = new Set<string>();\n for (const row of rows) {\n if (!repoMatchesFilter(row.repository, opts.filter)) continue;\n sessionIds.add(row.session_id);\n }\n\n return sessionIds;\n}\n\nconst TOKEN_CACHE_TTL_MS = 5 * 60 * 1000;\nconst tokenCache = new Map<string, { token: string; expiresAt: number }>();\n\nfunction resolveToken(target: SyncTarget): string | undefined {\n if (target.token) return target.token;\n if (!target.tokenCommand) return undefined;\n\n const cached = tokenCache.get(target.name);\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n\n try {\n const token = execSync(target.tokenCommand, {\n encoding: \"utf-8\",\n timeout: 10_000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n if (token) {\n tokenCache.set(target.name, {\n token,\n expiresAt: Date.now() + TOKEN_CACHE_TTL_MS,\n });\n }\n return token || undefined;\n } catch (err) {\n log.sync.error(\n `tokenCommand failed for \"${target.name}\": ${err instanceof Error ? err.message : err}`,\n );\n return undefined;\n }\n}\n\nexport function createSyncLoop(opts: SyncOptions): SyncHandle {\n const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n const postBatchSize = opts.postBatchSize ?? DEFAULT_POST_BATCH_SIZE;\n const idleMs = opts.idleIntervalMs ?? DEFAULT_IDLE_MS;\n const catchUpMs = opts.catchUpIntervalMs ?? DEFAULT_CATCHUP_MS;\n\n const panopticonVersion =\n typeof __PANOPTICON_VERSION__ !== \"undefined\"\n ? __PANOPTICON_VERSION__\n : \"dev\";\n\n function resolveHeaders(target: SyncTarget): Record<string, string> {\n const headers: Record<string, string> = { ...target.headers };\n headers[\"X-Panopticon-Version\"] = panopticonVersion;\n const token = resolveToken(target);\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n return headers;\n }\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n let syncing = false;\n let stopping = false;\n\n function scheduleNext(hadWork: boolean): void {\n if (stopping) return;\n const delay = hadWork ? catchUpMs : idleMs;\n timer = setTimeout(() => {\n tick().catch((err) => log.sync.error(`Tick error: ${err}`));\n }, delay);\n if (!opts.keepAlive && timer.unref) {\n timer.unref();\n }\n }\n\n // ── Phase 1: Sync sessions (watermark on sync_seq, repo-filtered) ────────\n\n async function syncSessions(\n target: SyncTarget,\n syncableSessionIds: Set<string> | null,\n ): Promise<boolean> {\n const sessionsDesc = TABLE_SYNC_REGISTRY.find(\n (d) => d.table === \"sessions\",\n );\n if (!sessionsDesc) return false;\n\n const wmKey = watermarkKey(sessionsDesc.table, target.name);\n const wm = readWatermark(wmKey);\n const { rows, maxId } = sessionsDesc.read(wm, batchSize);\n if (rows.length === 0) return false;\n\n // Filter by repo attribution\n let filtered = rows;\n if (syncableSessionIds) {\n filtered = rows.filter((r: unknown) => {\n const row = r as Record<string, unknown>;\n const sessionId =\n (row.sessionId as string) ?? (row.session_id as string);\n return sessionId && syncableSessionIds.has(sessionId);\n });\n }\n\n if (filtered.length > 0) {\n log.sync.info(\n `sessions: ${filtered.length} sessions (watermark ${wm} → ${maxId})`,\n );\n\n // POST in batches and collect accepted session IDs\n for (let j = 0; j < filtered.length; j += postBatchSize) {\n const batch = filtered.slice(j, j + postBatchSize);\n const response = await postSync(\n `${target.url}/v1/sync`,\n { table: \"sessions\", rows: batch },\n resolveHeaders(target),\n );\n\n // Record confirmed sessions from backend response\n const accepted = response.accepted;\n if (Array.isArray(accepted)) {\n recordConfirmedSessions(accepted as string[], target.name);\n }\n }\n }\n\n writeWatermark(wmKey, maxId);\n return rows.length === batchSize;\n }\n\n // ── Phase 2: Sync dependent data (per-session, gated by confirmed) ───────\n\n async function syncSessionData(target: SyncTarget): Promise<boolean> {\n const db = getDb();\n\n // Clean up orphaned entries (session deleted from local DB)\n db.prepare(\n `DELETE FROM target_session_sync\n WHERE session_id NOT IN (SELECT session_id FROM sessions)`,\n ).run();\n\n // Get confirmed sessions that have new data (sync_seq > synced_seq)\n const pending = db\n .prepare(\n `SELECT session_id, sync_seq,\n wm_messages, wm_tool_calls, wm_scanner_turns,\n wm_scanner_events, wm_hook_events, wm_otel_logs,\n wm_otel_metrics, wm_otel_spans\n FROM target_session_sync\n WHERE target = ? AND confirmed = 1\n AND sync_seq > synced_seq\n ORDER BY rowid\n LIMIT ?`,\n )\n .all(target.name, MAX_SESSIONS_PER_TICK) as Array<{\n session_id: string;\n sync_seq: number;\n wm_messages: number;\n wm_tool_calls: number;\n wm_scanner_turns: number;\n wm_scanner_events: number;\n wm_hook_events: number;\n wm_otel_logs: number;\n wm_otel_metrics: number;\n wm_otel_spans: number;\n }>;\n\n if (pending.length === 0) return false;\n\n const headers = resolveHeaders(target);\n const url = `${target.url}/v1/sync`;\n\n for (const entry of pending) {\n // Build watermarks from explicit columns\n const wmRow = entry as unknown as Record<string, number>;\n const watermarks: Record<string, number> = {};\n for (const [table, col] of Object.entries(WM_COLUMNS)) {\n watermarks[table] = wmRow[col] ?? 0;\n }\n let anyData = false;\n\n for (const [table, reader] of Object.entries(SESSION_READERS)) {\n // Read and POST in batches, draining until no more rows\n let afterId = watermarks[table] ?? 0;\n for (;;) {\n const { rows, maxId } = reader(entry.session_id, afterId, batchSize);\n if (rows.length === 0) break;\n\n anyData = true;\n\n for (let i = 0; i < rows.length; i += postBatchSize) {\n const batch = rows.slice(i, i + postBatchSize);\n await postSync(url, { table, rows: batch }, headers);\n }\n\n afterId = maxId;\n watermarks[table] = maxId;\n\n if (rows.length < batchSize) break;\n }\n }\n\n // Update watermarks and mark as synced up to cached sync_seq\n db.prepare(\n `UPDATE target_session_sync\n SET wm_messages = ?, wm_tool_calls = ?, wm_scanner_turns = ?,\n wm_scanner_events = ?, wm_hook_events = ?, wm_otel_logs = ?,\n wm_otel_metrics = ?, wm_otel_spans = ?, synced_seq = ?\n WHERE session_id = ? AND target = ?`,\n ).run(\n watermarks.messages,\n watermarks.tool_calls,\n watermarks.scanner_turns,\n watermarks.scanner_events,\n watermarks.hook_events,\n watermarks.otel_logs,\n watermarks.otel_metrics,\n watermarks.otel_spans,\n entry.sync_seq,\n entry.session_id,\n target.name,\n );\n\n if (anyData) {\n log.sync.info(\n `session-sync: synced data for ${entry.session_id} to ${target.name}`,\n );\n }\n }\n\n // Check if more confirmed sessions with new data remain\n const remaining = db\n .prepare(\n `SELECT COUNT(*) as cnt FROM target_session_sync\n WHERE target = ? AND confirmed = 1\n AND sync_seq > synced_seq`,\n )\n .get(target.name) as { cnt: number };\n\n return remaining.cnt > 0;\n }\n\n // ── Phase 3: Sync non-session tables (unchanged) ─────────────────────────\n\n async function syncNonSessionTables(target: SyncTarget): Promise<boolean> {\n let hasMore = false;\n\n for (const desc of TABLE_SYNC_REGISTRY) {\n if (desc.sessionLinked) continue; // handled by Phase 1 and 2\n\n const wmKey = watermarkKey(desc.table, target.name);\n const wm = readWatermark(wmKey);\n const { rows, maxId } = desc.read(wm, batchSize);\n if (rows.length === 0) continue;\n\n // Apply repo filter to repo_config_snapshots\n const filtered =\n desc.table === \"repo_config_snapshots\"\n ? rows.filter((r: unknown) => {\n const row = r as Record<string, unknown>;\n return repoMatchesFilter(row.repository as string, opts.filter);\n })\n : rows;\n\n if (filtered.length > 0) {\n log.sync.info(\n `${desc.table}: ${filtered.length} ${desc.logNoun} (watermark ${wm} → ${maxId})`,\n );\n\n for (let j = 0; j < filtered.length; j += postBatchSize) {\n const batch = filtered.slice(j, j + postBatchSize);\n await postSync(\n `${target.url}/v1/sync`,\n { table: desc.table, rows: batch },\n resolveHeaders(target),\n );\n }\n }\n\n writeWatermark(wmKey, maxId);\n if (rows.length === batchSize) hasMore = true;\n }\n\n return hasMore;\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n function recordConfirmedSessions(\n sessionIds: string[],\n targetName: string,\n ): void {\n const db = getDb();\n const upsert = db.prepare(\n `INSERT INTO target_session_sync (session_id, target, confirmed, sync_seq)\n VALUES (?, ?, 1, (SELECT sync_seq FROM sessions WHERE session_id = ?))\n ON CONFLICT(session_id, target) DO UPDATE SET\n confirmed = 1,\n sync_seq = MAX(target_session_sync.sync_seq,\n (SELECT sync_seq FROM sessions WHERE session_id = excluded.session_id))`,\n );\n for (const sessionId of sessionIds) {\n upsert.run(sessionId, targetName, sessionId);\n }\n }\n\n // ── Main loop ────────────────────────────────────────────────────────────\n\n async function runOnce(): Promise<boolean> {\n let hasMore = false;\n\n const syncableSessionIds = buildSyncableSessionIds(opts);\n\n for (const target of opts.targets) {\n try {\n // Phase 1: Sync sessions (repo-filtered, backend confirms)\n if (await syncSessions(target, syncableSessionIds)) hasMore = true;\n\n // Phase 2: Sync dependent data for confirmed sessions\n if (await syncSessionData(target)) hasMore = true;\n\n // Phase 3: Sync non-session tables\n if (await syncNonSessionTables(target)) hasMore = true;\n } catch (err) {\n log.sync.error(\n `Error syncing to ${target.name}: ${err instanceof Error ? err.message : err}`,\n );\n captureException(err, {\n component: \"sync\",\n target: target.name,\n });\n }\n }\n\n return hasMore;\n }\n\n async function tick(): Promise<void> {\n if (syncing || stopping) return;\n syncing = true;\n let hasMore = false;\n try {\n getDb(); // ensure DB is accessible\n hasMore = await runOnce();\n } catch (err) {\n log.sync.error(\n `Cycle error: ${err instanceof Error ? (err.stack ?? err.message) : err}`,\n );\n captureException(err, { component: \"sync\" });\n } finally {\n syncing = false;\n }\n\n if (!stopping) {\n scheduleNext(hasMore);\n }\n }\n\n return {\n start() {\n if (timer || syncing) return;\n stopping = false;\n log.sync.info(\"Starting sync\");\n tick().catch((err) => log.sync.error(`Tick error: ${err}`));\n },\n stop() {\n stopping = true;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n log.sync.info(\"Stopped sync\");\n }\n },\n };\n}\n","const MAX_RETRIES = 5;\nconst BASE_DELAY_MS = 2000;\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport async function postSync(\n url: string,\n body: { table: string; rows: unknown[] },\n headers: Record<string, string>,\n): Promise<Record<string, unknown>> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n REQUEST_TIMEOUT_MS,\n );\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...headers },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n clearTimeout(timeoutId);\n\n if (response.ok) {\n const text = await response.text().catch(() => \"\");\n try {\n return text ? (JSON.parse(text) as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n }\n\n const status = response.status;\n\n // Don't retry client errors (except 429 rate limit)\n if (status >= 400 && status < 500 && status !== 429) {\n const text = await response.text().catch(() => \"\");\n throw new Error(`HTTP ${status}: ${text}`);\n }\n\n lastError = new Error(`HTTP ${status}`);\n } catch (err) {\n if (\n err instanceof Error &&\n err.message.startsWith(\"HTTP 4\") &&\n !err.message.startsWith(\"HTTP 429\")\n ) {\n throw err;\n }\n lastError = err instanceof Error ? err : new Error(String(err));\n }\n\n if (attempt < MAX_RETRIES) {\n const delay = BASE_DELAY_MS * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n\n throw lastError ?? new Error(\"postSync failed\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,134 @@
1
+ import {
2
+ getDb
3
+ } from "./chunk-DZ5HJFB4.js";
4
+ import {
5
+ config
6
+ } from "./chunk-K7YUPLES.js";
7
+
8
+ // src/db/prune.ts
9
+ import fs from "fs";
10
+ function pruneEstimate(cutoffMs) {
11
+ const db = getDb();
12
+ const cutoffNs = cutoffMs * 1e6;
13
+ const logs = db.prepare("SELECT COUNT(*) as c FROM otel_logs WHERE timestamp_ns < ?").get(cutoffNs).c;
14
+ const metrics = db.prepare("SELECT COUNT(*) as c FROM otel_metrics WHERE timestamp_ns < ?").get(cutoffNs).c;
15
+ const hooks = db.prepare("SELECT COUNT(*) as c FROM hook_events WHERE timestamp_ms < ?").get(cutoffMs).c;
16
+ const sessionRepos = db.prepare(
17
+ "SELECT COUNT(*) as c FROM session_repositories WHERE first_seen_ms < ?"
18
+ ).get(cutoffMs).c;
19
+ const sessionCwds = db.prepare("SELECT COUNT(*) as c FROM session_cwds WHERE first_seen_ms < ?").get(cutoffMs).c;
20
+ const pricing = db.prepare(
21
+ `SELECT COUNT(*) as c FROM model_pricing
22
+ WHERE updated_ms < ?
23
+ AND id NOT IN (
24
+ SELECT id FROM (
25
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY model_id ORDER BY updated_ms DESC) as rn
26
+ FROM model_pricing
27
+ ) WHERE rn = 1
28
+ )`
29
+ ).get(cutoffMs).c;
30
+ const sessionsToDelete = "SELECT session_id FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?";
31
+ const sessions = db.prepare(
32
+ "SELECT COUNT(*) as c FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?"
33
+ ).get(cutoffMs).c;
34
+ const messages = db.prepare(
35
+ `SELECT COUNT(*) as c FROM messages WHERE session_id IN (${sessionsToDelete})`
36
+ ).get(cutoffMs).c;
37
+ const toolCalls = db.prepare(
38
+ `SELECT COUNT(*) as c FROM tool_calls WHERE session_id IN (${sessionsToDelete})`
39
+ ).get(cutoffMs).c;
40
+ const scannerTurns = db.prepare(
41
+ `SELECT COUNT(*) as c FROM scanner_turns WHERE session_id IN (${sessionsToDelete})`
42
+ ).get(cutoffMs).c;
43
+ const scannerEvents = db.prepare(
44
+ `SELECT COUNT(*) as c FROM scanner_events WHERE session_id IN (${sessionsToDelete})`
45
+ ).get(cutoffMs).c;
46
+ return {
47
+ otel_logs: logs,
48
+ otel_metrics: metrics,
49
+ hook_events: hooks,
50
+ session_repositories: sessionRepos,
51
+ session_cwds: sessionCwds,
52
+ model_pricing: pricing,
53
+ sessions,
54
+ messages,
55
+ tool_calls: toolCalls,
56
+ scanner_turns: scannerTurns,
57
+ scanner_events: scannerEvents
58
+ };
59
+ }
60
+ function pruneExecute(cutoffMs) {
61
+ const db = getDb();
62
+ const cutoffNs = cutoffMs * 1e6;
63
+ const tx = db.transaction(() => {
64
+ const logs = db.prepare("DELETE FROM otel_logs WHERE timestamp_ns < ?").run(cutoffNs).changes;
65
+ const metrics = db.prepare("DELETE FROM otel_metrics WHERE timestamp_ns < ?").run(cutoffNs).changes;
66
+ db.prepare(
67
+ "DELETE FROM hook_events_fts WHERE rowid IN (SELECT id FROM hook_events WHERE timestamp_ms < ?)"
68
+ ).run(cutoffMs);
69
+ const hooks = db.prepare("DELETE FROM hook_events WHERE timestamp_ms < ?").run(cutoffMs).changes;
70
+ const sessionRepos = db.prepare("DELETE FROM session_repositories WHERE first_seen_ms < ?").run(cutoffMs).changes;
71
+ const sessionCwds = db.prepare("DELETE FROM session_cwds WHERE first_seen_ms < ?").run(cutoffMs).changes;
72
+ const pricing = db.prepare(
73
+ `DELETE FROM model_pricing
74
+ WHERE updated_ms < ?
75
+ AND id NOT IN (
76
+ SELECT id FROM (
77
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY model_id ORDER BY updated_ms DESC) as rn
78
+ FROM model_pricing
79
+ ) WHERE rn = 1
80
+ )`
81
+ ).run(cutoffMs).changes;
82
+ const oldSessions = "SELECT session_id FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?";
83
+ db.prepare(
84
+ `DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id IN (${oldSessions}))`
85
+ ).run(cutoffMs);
86
+ const messages = db.prepare(`DELETE FROM messages WHERE session_id IN (${oldSessions})`).run(cutoffMs).changes;
87
+ const toolCalls = db.prepare(`DELETE FROM tool_calls WHERE session_id IN (${oldSessions})`).run(cutoffMs).changes;
88
+ const scannerTurns = db.prepare(`DELETE FROM scanner_turns WHERE session_id IN (${oldSessions})`).run(cutoffMs).changes;
89
+ const scannerEvents = db.prepare(
90
+ `DELETE FROM scanner_events WHERE session_id IN (${oldSessions})`
91
+ ).run(cutoffMs).changes;
92
+ db.prepare(
93
+ "DELETE FROM scanner_file_watermarks WHERE file_path IN (SELECT scanner_file_path FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ? AND scanner_file_path IS NOT NULL)"
94
+ ).run(cutoffMs);
95
+ db.prepare(
96
+ `DELETE FROM session_summary_deltas WHERE session_id IN (${oldSessions})`
97
+ ).run(cutoffMs);
98
+ const sessions = db.prepare(
99
+ "DELETE FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?"
100
+ ).run(cutoffMs).changes;
101
+ return {
102
+ otel_logs: logs,
103
+ otel_metrics: metrics,
104
+ hook_events: hooks,
105
+ session_repositories: sessionRepos,
106
+ session_cwds: sessionCwds,
107
+ model_pricing: pricing,
108
+ sessions,
109
+ messages,
110
+ tool_calls: toolCalls,
111
+ scanner_turns: scannerTurns,
112
+ scanner_events: scannerEvents
113
+ };
114
+ });
115
+ return tx();
116
+ }
117
+ function autoPrune(maxAgeDays, maxSizeMb) {
118
+ let sizeBytes;
119
+ try {
120
+ sizeBytes = fs.statSync(config.dbPath).size;
121
+ } catch {
122
+ return;
123
+ }
124
+ if (sizeBytes / (1024 * 1024) <= maxSizeMb) return;
125
+ const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
126
+ pruneExecute(cutoffMs);
127
+ }
128
+
129
+ export {
130
+ pruneEstimate,
131
+ pruneExecute,
132
+ autoPrune
133
+ };
134
+ //# sourceMappingURL=chunk-HRCEIYKU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/prune.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { config } from \"../config.js\";\nimport { getDb } from \"./schema.js\";\n\nexport interface PruneResult {\n otel_logs: number;\n otel_metrics: number;\n hook_events: number;\n session_repositories: number;\n session_cwds: number;\n model_pricing: number;\n sessions: number;\n messages: number;\n tool_calls: number;\n scanner_turns: number;\n scanner_events: number;\n}\n\nexport function pruneEstimate(cutoffMs: number): PruneResult {\n const db = getDb();\n const cutoffNs = cutoffMs * 1_000_000;\n\n const logs = (\n db\n .prepare(\"SELECT COUNT(*) as c FROM otel_logs WHERE timestamp_ns < ?\")\n .get(cutoffNs) as { c: number }\n ).c;\n const metrics = (\n db\n .prepare(\"SELECT COUNT(*) as c FROM otel_metrics WHERE timestamp_ns < ?\")\n .get(cutoffNs) as { c: number }\n ).c;\n const hooks = (\n db\n .prepare(\"SELECT COUNT(*) as c FROM hook_events WHERE timestamp_ms < ?\")\n .get(cutoffMs) as { c: number }\n ).c;\n const sessionRepos = (\n db\n .prepare(\n \"SELECT COUNT(*) as c FROM session_repositories WHERE first_seen_ms < ?\",\n )\n .get(cutoffMs) as { c: number }\n ).c;\n const sessionCwds = (\n db\n .prepare(\"SELECT COUNT(*) as c FROM session_cwds WHERE first_seen_ms < ?\")\n .get(cutoffMs) as { c: number }\n ).c;\n\n // Pricing: count rows older than cutoff, excluding the latest per model\n const pricing = (\n db\n .prepare(\n `SELECT COUNT(*) as c FROM model_pricing\n WHERE updated_ms < ?\n AND id NOT IN (\n SELECT id FROM (\n SELECT id, ROW_NUMBER() OVER (PARTITION BY model_id ORDER BY updated_ms DESC) as rn\n FROM model_pricing\n ) WHERE rn = 1\n )`,\n )\n .get(cutoffMs) as { c: number }\n ).c;\n\n // Scanner-derived data pruned by session age\n const sessionsToDelete =\n \"SELECT session_id FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?\";\n const sessions = (\n db\n .prepare(\n \"SELECT COUNT(*) as c FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?\",\n )\n .get(cutoffMs) as { c: number }\n ).c;\n const messages = (\n db\n .prepare(\n `SELECT COUNT(*) as c FROM messages WHERE session_id IN (${sessionsToDelete})`,\n )\n .get(cutoffMs) as { c: number }\n ).c;\n const toolCalls = (\n db\n .prepare(\n `SELECT COUNT(*) as c FROM tool_calls WHERE session_id IN (${sessionsToDelete})`,\n )\n .get(cutoffMs) as { c: number }\n ).c;\n const scannerTurns = (\n db\n .prepare(\n `SELECT COUNT(*) as c FROM scanner_turns WHERE session_id IN (${sessionsToDelete})`,\n )\n .get(cutoffMs) as { c: number }\n ).c;\n const scannerEvents = (\n db\n .prepare(\n `SELECT COUNT(*) as c FROM scanner_events WHERE session_id IN (${sessionsToDelete})`,\n )\n .get(cutoffMs) as { c: number }\n ).c;\n\n return {\n otel_logs: logs,\n otel_metrics: metrics,\n hook_events: hooks,\n session_repositories: sessionRepos,\n session_cwds: sessionCwds,\n model_pricing: pricing,\n sessions,\n messages,\n tool_calls: toolCalls,\n scanner_turns: scannerTurns,\n scanner_events: scannerEvents,\n };\n}\n\nexport function pruneExecute(cutoffMs: number): PruneResult {\n const db = getDb();\n const cutoffNs = cutoffMs * 1_000_000;\n\n const tx = db.transaction(() => {\n const logs = db\n .prepare(\"DELETE FROM otel_logs WHERE timestamp_ns < ?\")\n .run(cutoffNs).changes;\n const metrics = db\n .prepare(\"DELETE FROM otel_metrics WHERE timestamp_ns < ?\")\n .run(cutoffNs).changes;\n\n // Delete from FTS5 index before deleting from hook_events\n db.prepare(\n \"DELETE FROM hook_events_fts WHERE rowid IN (SELECT id FROM hook_events WHERE timestamp_ms < ?)\",\n ).run(cutoffMs);\n\n const hooks = db\n .prepare(\"DELETE FROM hook_events WHERE timestamp_ms < ?\")\n .run(cutoffMs).changes;\n\n const sessionRepos = db\n .prepare(\"DELETE FROM session_repositories WHERE first_seen_ms < ?\")\n .run(cutoffMs).changes;\n const sessionCwds = db\n .prepare(\"DELETE FROM session_cwds WHERE first_seen_ms < ?\")\n .run(cutoffMs).changes;\n\n // Pricing: delete rows older than cutoff, always keeping the latest per model\n const pricing = db\n .prepare(\n `DELETE FROM model_pricing\n WHERE updated_ms < ?\n AND id NOT IN (\n SELECT id FROM (\n SELECT id, ROW_NUMBER() OVER (PARTITION BY model_id ORDER BY updated_ms DESC) as rn\n FROM model_pricing\n ) WHERE rn = 1\n )`,\n )\n .run(cutoffMs).changes;\n\n // Prune scanner-derived data for old sessions\n const oldSessions =\n \"SELECT session_id FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?\";\n\n // FTS before messages\n db.prepare(\n `DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id IN (${oldSessions}))`,\n ).run(cutoffMs);\n const messages = db\n .prepare(`DELETE FROM messages WHERE session_id IN (${oldSessions})`)\n .run(cutoffMs).changes;\n const toolCalls = db\n .prepare(`DELETE FROM tool_calls WHERE session_id IN (${oldSessions})`)\n .run(cutoffMs).changes;\n const scannerTurns = db\n .prepare(`DELETE FROM scanner_turns WHERE session_id IN (${oldSessions})`)\n .run(cutoffMs).changes;\n const scannerEvents = db\n .prepare(\n `DELETE FROM scanner_events WHERE session_id IN (${oldSessions})`,\n )\n .run(cutoffMs).changes;\n // Delete watermarks for files backing these sessions\n db.prepare(\n \"DELETE FROM scanner_file_watermarks WHERE file_path IN (SELECT scanner_file_path FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ? AND scanner_file_path IS NOT NULL)\",\n ).run(cutoffMs);\n // Summary deltas\n db.prepare(\n `DELETE FROM session_summary_deltas WHERE session_id IN (${oldSessions})`,\n ).run(cutoffMs);\n const sessions = db\n .prepare(\n \"DELETE FROM sessions WHERE started_at_ms IS NOT NULL AND started_at_ms < ?\",\n )\n .run(cutoffMs).changes;\n\n return {\n otel_logs: logs,\n otel_metrics: metrics,\n hook_events: hooks,\n session_repositories: sessionRepos,\n session_cwds: sessionCwds,\n model_pricing: pricing,\n sessions,\n messages,\n tool_calls: toolCalls,\n scanner_turns: scannerTurns,\n scanner_events: scannerEvents,\n };\n });\n\n return tx();\n}\n\nexport function autoPrune(maxAgeDays: number, maxSizeMb: number): void {\n let sizeBytes: number;\n try {\n sizeBytes = fs.statSync(config.dbPath).size;\n } catch {\n return; // DB file doesn't exist yet\n }\n\n if (sizeBytes / (1024 * 1024) <= maxSizeMb) return;\n\n const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;\n pruneExecute(cutoffMs);\n}\n"],"mappings":"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","names":[]}