@fml-inc/panopticon 0.1.1 → 0.1.3

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 (63) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/client.js +1 -1
  3. package/dist/{chunk-KLXRBD4N.js → chunk-2PMFNSGA.js} +6 -5
  4. package/dist/{chunk-KLXRBD4N.js.map → chunk-2PMFNSGA.js.map} +1 -1
  5. package/dist/{chunk-3BUJ7URA.js → chunk-3ILOOWUF.js} +66 -2
  6. package/dist/chunk-3ILOOWUF.js.map +1 -0
  7. package/dist/{chunk-NXH7AONS.js → chunk-5FRYHDPJ.js} +8 -6
  8. package/dist/chunk-5FRYHDPJ.js.map +1 -0
  9. package/dist/{chunk-TCKL7E4K.js → chunk-5IPC5XXZ.js} +4 -4
  10. package/dist/{chunk-YZBYEULL.js → chunk-75IXBXBO.js} +5 -5
  11. package/dist/{chunk-HRCEIYKU.js → chunk-7NBZ4NMJ.js} +2 -2
  12. package/dist/{chunk-4SM2H22C.js → chunk-HO443ZQM.js} +1 -1
  13. package/dist/{chunk-4SM2H22C.js.map → chunk-HO443ZQM.js.map} +1 -1
  14. package/dist/{chunk-3TZAKV3M.js → chunk-OSWBZKK5.js} +2 -2
  15. package/dist/{chunk-SEXU2WYG.js → chunk-P5KAIE3O.js} +5 -4
  16. package/dist/chunk-P5KAIE3O.js.map +1 -0
  17. package/dist/{chunk-XO5NQRTD.js → chunk-RNICEX6N.js} +2 -2
  18. package/dist/{chunk-L7G27XWF.js → chunk-RRHBTXHB.js} +3 -3
  19. package/dist/{chunk-BVOE7A2Z.js → chunk-UD6H6EP6.js} +8 -6
  20. package/dist/chunk-UD6H6EP6.js.map +1 -0
  21. package/dist/{chunk-LWXF7YRG.js → chunk-UDUBRSIG.js} +2 -2
  22. package/dist/{chunk-SUGSQ4YI.js → chunk-UEEWG7FD.js} +4 -4
  23. package/dist/{chunk-DZ5HJFB4.js → chunk-UM5U6FPH.js} +60 -2
  24. package/dist/chunk-UM5U6FPH.js.map +1 -0
  25. package/dist/{chunk-VXQ33OYT.js → chunk-YV2AN73J.js} +47 -19
  26. package/dist/chunk-YV2AN73J.js.map +1 -0
  27. package/dist/cli.js +6 -6
  28. package/dist/cli.js.map +1 -1
  29. package/dist/db.js +1 -1
  30. package/dist/doctor.js +4 -4
  31. package/dist/hooks/handler.js +4 -4
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -15
  34. package/dist/mcp/server.js +1 -1
  35. package/dist/otlp/server.js +5 -5
  36. package/dist/pricing.js +2 -2
  37. package/dist/proxy/server.js +5 -5
  38. package/dist/prune.js +2 -2
  39. package/dist/query.js +2 -2
  40. package/dist/{reparse-636YZCE3.js → reparse-TFYD67WP.js} +5 -5
  41. package/dist/scanner.d.ts +7 -1
  42. package/dist/scanner.js +1 -1
  43. package/dist/server.js +13 -13
  44. package/dist/setup.js +3 -3
  45. package/dist/sync/index.d.ts +2 -2
  46. package/dist/sync/index.js +4 -4
  47. package/dist/{types-D-MYCBol.d.ts → types-DrhrWbWe.d.ts} +1 -0
  48. package/package.json +1 -1
  49. package/dist/chunk-3BUJ7URA.js.map +0 -1
  50. package/dist/chunk-BVOE7A2Z.js.map +0 -1
  51. package/dist/chunk-DZ5HJFB4.js.map +0 -1
  52. package/dist/chunk-NXH7AONS.js.map +0 -1
  53. package/dist/chunk-SEXU2WYG.js.map +0 -1
  54. package/dist/chunk-VXQ33OYT.js.map +0 -1
  55. /package/dist/{chunk-TCKL7E4K.js.map → chunk-5IPC5XXZ.js.map} +0 -0
  56. /package/dist/{chunk-YZBYEULL.js.map → chunk-75IXBXBO.js.map} +0 -0
  57. /package/dist/{chunk-HRCEIYKU.js.map → chunk-7NBZ4NMJ.js.map} +0 -0
  58. /package/dist/{chunk-3TZAKV3M.js.map → chunk-OSWBZKK5.js.map} +0 -0
  59. /package/dist/{chunk-XO5NQRTD.js.map → chunk-RNICEX6N.js.map} +0 -0
  60. /package/dist/{chunk-L7G27XWF.js.map → chunk-RRHBTXHB.js.map} +0 -0
  61. /package/dist/{chunk-LWXF7YRG.js.map → chunk-UDUBRSIG.js.map} +0 -0
  62. /package/dist/{chunk-SUGSQ4YI.js.map → chunk-UEEWG7FD.js.map} +0 -0
  63. /package/dist/{reparse-636YZCE3.js.map → reparse-TFYD67WP.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/scanner/reparse.ts","../src/scanner/loop.ts","../src/archive/local.ts","../src/archive/index.ts","../src/summary/llm.ts","../src/summary/loop.ts","../src/scanner/store.ts"],"sourcesContent":["/**\n * Atomic DB reparse: builds a fresh database from scratch, copies\n * non-scanner metadata from the old DB, then swaps files atomically.\n *\n * This avoids the cost of row-by-row deletes on large databases and\n * ensures that parser changes (tracked via SCANNER_DATA_VERSION) are\n * applied cleanly to all existing session data.\n *\n * Preserves sync_id values for rows that match on natural keys so\n * that remote sync targets can deduplicate correctly.\n */\nimport fs from \"node:fs\";\nimport { gunzipSync } from \"node:zlib\";\nimport Database from \"better-sqlite3\";\nimport { config } from \"../config.js\";\nimport {\n closeDb,\n getDb,\n SCANNER_DATA_VERSION,\n SCHEMA_SQL,\n} from \"../db/schema.js\";\nimport { scanOnce } from \"./loop.js\";\n\n/**\n * Tables whose data is independent of the scanner and must be\n * preserved across reparse. Copied row-by-row from old → new DB.\n */\nconst PRESERVED_TABLES = [\n \"hook_events\",\n \"otel_logs\",\n \"otel_metrics\",\n \"otel_spans\",\n \"watermarks\",\n \"target_session_sync\",\n \"model_pricing\",\n \"user_config_snapshots\",\n \"repo_config_snapshots\",\n];\n\n/**\n * Session columns that come from non-scanner sources (hooks, OTLP)\n * and should be merged back after the scanner rebuilds sessions.\n */\nconst SESSION_MERGE_COLUMNS = [\n \"has_hooks\",\n \"has_otel\",\n \"otel_input_tokens\",\n \"otel_output_tokens\",\n \"otel_cache_read_tokens\",\n \"otel_cache_creation_tokens\",\n \"summary\",\n \"summary_version\",\n \"permission_mode\",\n \"is_automated\",\n \"created_at\",\n];\n\nexport interface ReparseResult {\n success: boolean;\n filesScanned: number;\n newTurns: number;\n error?: string;\n}\n\nfunction removeTempFiles(tempPath: string): void {\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) {\n try {\n fs.unlinkSync(tempPath + suffix);\n } catch {}\n }\n}\n\nfunction removeWAL(dbPath: string): void {\n for (const suffix of [\"-wal\", \"-shm\"]) {\n try {\n fs.unlinkSync(dbPath + suffix);\n } catch {}\n }\n}\n\nfunction initTempDb(tempPath: string): Database.Database {\n const db = new Database(tempPath);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.function(\"decompress\", (blob: Buffer | null) =>\n blob ? gunzipSync(blob).toString() : null,\n );\n db.exec(SCHEMA_SQL);\n return db;\n}\n\n/**\n * Perform an atomic reparse:\n * 1. Close current DB\n * 2. Create a fresh temp DB with current schema\n * 3. Redirect config.dbPath → temp, run full scan into it\n * 4. Copy preserved (non-scanner) data from old DB via ATTACH\n * 5. Merge session metadata from hooks/OTLP\n * 6. Preserve sync_id values for rows matching on natural keys\n * 7. Atomic file swap (rename)\n * 8. Reopen the main DB handle\n */\nexport function reparseAll(\n log: (msg: string) => void = () => {},\n): ReparseResult {\n const origPath = config.dbPath;\n const tempPath = `${origPath}-reparse`;\n\n // Clean up stale temp DB from a prior crash\n removeTempFiles(tempPath);\n\n log(\"Starting atomic reparse...\");\n\n // 1. Create fresh temp DB and verify schema\n let tempDb: Database.Database;\n try {\n tempDb = initTempDb(tempPath);\n tempDb.close();\n } catch (err) {\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned: 0,\n newTurns: 0,\n error: `Failed to create temp DB: ${err}`,\n };\n }\n\n // Snapshot old session count for safety check\n let oldSessionCount = 0;\n try {\n const oldDb = new Database(origPath);\n oldSessionCount = (\n oldDb.prepare(\"SELECT COUNT(*) as c FROM sessions\").get() as {\n c: number;\n }\n ).c;\n oldDb.close();\n } catch {}\n\n // 2. Close current DB, redirect to temp, scan\n closeDb();\n const savedDbPath = config.dbPath;\n (config as { dbPath: string }).dbPath = tempPath;\n\n let filesScanned = 0;\n let newTurns = 0;\n try {\n const result = scanOnce();\n filesScanned = result.filesScanned;\n newTurns = result.newTurns;\n } catch (err) {\n (config as { dbPath: string }).dbPath = savedDbPath;\n closeDb();\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned: 0,\n newTurns: 0,\n error: `Scan into temp DB failed: ${err}`,\n };\n }\n\n // Check session count in temp DB\n const db = getDb();\n const tempSessionCount = (\n db.prepare(\"SELECT COUNT(*) as c FROM sessions\").get() as { c: number }\n ).c;\n closeDb();\n\n // Restore config path for all subsequent operations\n (config as { dbPath: string }).dbPath = savedDbPath;\n\n // Abort if scan produced nothing but old DB had data\n if (tempSessionCount === 0 && oldSessionCount > 0) {\n log(\n `Reparse aborted: temp DB has 0 sessions but old DB has ${oldSessionCount}`,\n );\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Aborted: 0 sessions in reparse vs ${oldSessionCount} in old DB`,\n };\n }\n\n // 3. Copy preserved data from old DB into temp DB\n log(\"Copying preserved data from old database...\");\n try {\n tempDb = new Database(tempPath);\n tempDb.pragma(\"journal_mode = WAL\");\n tempDb.function(\"decompress\", (blob: Buffer | null) =>\n blob ? gunzipSync(blob).toString() : null,\n );\n const escapedPath = origPath.replace(/'/g, \"''\");\n tempDb.exec(`ATTACH DATABASE '${escapedPath}' AS old_db`);\n\n const tx = tempDb.transaction(() => {\n for (const table of PRESERVED_TABLES) {\n try {\n tempDb.exec(\n `INSERT OR IGNORE INTO main.${table} SELECT * FROM old_db.${table}`,\n );\n } catch (e) {\n log(` Skipping ${table}: ${e instanceof Error ? e.message : e}`);\n }\n }\n\n // Rebuild hook_events_fts from copied hook_events\n try {\n tempDb.exec(\n \"INSERT INTO main.hook_events_fts(rowid, payload) SELECT id, decompress(payload) FROM main.hook_events\",\n );\n } catch (e) {\n log(` hook_events_fts rebuild: ${e instanceof Error ? e.message : e}`);\n }\n\n // Merge session metadata from hooks/OTLP into scanner-created sessions\n const setClauses = SESSION_MERGE_COLUMNS.map(\n (col) => `${col} = old_db.sessions.${col}`,\n ).join(\", \");\n try {\n tempDb.exec(`\n UPDATE main.sessions SET ${setClauses}\n FROM old_db.sessions\n WHERE main.sessions.session_id = old_db.sessions.session_id\n `);\n } catch (e) {\n log(` Session merge: ${e instanceof Error ? e.message : e}`);\n }\n\n // Copy session_repositories and session_cwds\n try {\n tempDb.exec(\n \"INSERT OR IGNORE INTO main.session_repositories SELECT * FROM old_db.session_repositories\",\n );\n } catch (e) {\n log(` session_repositories: ${e instanceof Error ? e.message : e}`);\n }\n try {\n tempDb.exec(\n \"INSERT OR IGNORE INTO main.session_cwds SELECT * FROM old_db.session_cwds\",\n );\n } catch (e) {\n log(` session_cwds: ${e instanceof Error ? e.message : e}`);\n }\n\n // Preserve sync_id values from old DB for scanner-produced tables\n // by matching on natural keys\n try {\n tempDb.exec(`\n UPDATE main.scanner_turns SET sync_id = old_db.scanner_turns.sync_id\n FROM old_db.scanner_turns\n WHERE main.scanner_turns.session_id = old_db.scanner_turns.session_id\n AND main.scanner_turns.source = old_db.scanner_turns.source\n AND main.scanner_turns.turn_index = old_db.scanner_turns.turn_index\n `);\n } catch (e) {\n log(` scanner_turns sync_id: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n tempDb.exec(`\n UPDATE main.scanner_events SET sync_id = old_db.scanner_events.sync_id\n FROM old_db.scanner_events\n WHERE main.scanner_events.session_id = old_db.scanner_events.session_id\n AND main.scanner_events.source = old_db.scanner_events.source\n AND main.scanner_events.event_type = old_db.scanner_events.event_type\n AND main.scanner_events.timestamp_ms = old_db.scanner_events.timestamp_ms\n AND COALESCE(main.scanner_events.tool_name, '') = COALESCE(old_db.scanner_events.tool_name, '')\n `);\n } catch (e) {\n log(` scanner_events sync_id: ${e instanceof Error ? e.message : e}`);\n }\n\n // Preserve sync_id for tool_calls by matching via message natural key + tool_use_id\n try {\n tempDb.exec(`\n UPDATE main.tool_calls SET sync_id = old_tc.sync_id\n FROM old_db.tool_calls old_tc\n INNER JOIN old_db.messages old_m ON old_tc.message_id = old_m.id\n INNER JOIN main.messages new_m ON new_m.session_id = old_m.session_id AND new_m.ordinal = old_m.ordinal\n WHERE main.tool_calls.message_id = new_m.id\n AND COALESCE(main.tool_calls.tool_use_id, '') = COALESCE(old_tc.tool_use_id, '')\n AND main.tool_calls.tool_name = old_tc.tool_name\n `);\n } catch (e) {\n log(` tool_calls sync_id: ${e instanceof Error ? e.message : e}`);\n }\n });\n tx();\n\n tempDb.exec(\"DETACH DATABASE old_db\");\n tempDb.pragma(`user_version = ${SCANNER_DATA_VERSION}`);\n tempDb.close();\n } catch (err) {\n log(`Failed to copy preserved data: ${err}`);\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Copy preserved data failed: ${err}`,\n };\n }\n\n // 4. Atomic file swap\n log(\"Swapping database files...\");\n try {\n removeWAL(origPath);\n fs.renameSync(tempPath, origPath);\n removeWAL(tempPath);\n } catch (err) {\n log(`File swap failed: ${err}`);\n getDb();\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Atomic swap failed: ${err}`,\n };\n }\n\n // 5. Reopen the main DB handle\n getDb();\n\n log(\n `Reparse complete: ${filesScanned} files, ${newTurns} turns, ${tempSessionCount} sessions`,\n );\n\n return { success: true, filesScanned, newTurns };\n}\n","import fs from \"node:fs\";\nimport type Database from \"better-sqlite3\";\nimport { getDb, markResyncComplete, needsResync } from \"../db/schema.js\";\nimport { updateSessionMessageCounts } from \"../db/store.js\";\n// Import targets so they self-register before we iterate the registry\nimport \"../targets/claude.js\";\nimport \"../targets/codex.js\";\nimport \"../targets/gemini.js\";\nimport { getArchiveBackend } from \"../archive/index.js\";\nimport { log } from \"../log.js\";\nimport { generateSummariesOnce } from \"../summary/index.js\";\nimport { allTargets } from \"../targets/registry.js\";\nimport type { ParseResult } from \"../targets/types.js\";\nimport type { SavedSyncIds } from \"./store.js\";\nimport {\n getMaxOrdinal,\n getTurnCount,\n insertMessages,\n insertScannerEvents,\n insertTurns,\n linkSubagentSessions,\n readArchivedSize,\n readFileWatermark,\n resetFileForReparse,\n restoreSyncIds,\n updateSessionTotals,\n upsertSession,\n writeArchivedSize,\n writeFileWatermark,\n} from \"./store.js\";\nimport type { ScannerHandle, ScannerOptions } from \"./types.js\";\n\nconst DEFAULT_IDLE_MS = 60_000;\nconst DEFAULT_CATCHUP_MS = 5_000;\n\nexport function scanOnce(): {\n filesScanned: number;\n newTurns: number;\n} {\n getDb(); // ensure DB is accessible\n\n let filesScanned = 0;\n let newTurns = 0;\n\n for (const target of allTargets()) {\n if (!target.scanner) continue;\n const source = target.id;\n\n for (const { filePath } of target.scanner.discover()) {\n let offset = readFileWatermark(filePath);\n let result = target.scanner.parseFile(filePath, offset);\n if (!result) continue;\n\n // If incremental parse detected a DAG fork, reset watermark\n // and reparse from byte 0 so fork detection runs on the full file.\n let savedSyncIds: SavedSyncIds | undefined;\n if (result.needsFullReparse && offset > 0) {\n savedSyncIds = resetFileForReparse(filePath, result.meta?.sessionId);\n offset = 0;\n result = target.scanner.parseFile(filePath, 0);\n if (!result) continue;\n log.scanner.info(`Reparsing ${filePath} from start (fork detected)`);\n }\n\n filesScanned++;\n\n // When reading from byte 0 (full file), turn indices start at 0 so\n // INSERT OR IGNORE deduplicates. When incremental (offset > 0),\n // re-index from existing turn count — unless the parser produces\n // absolute indices (e.g. Gemini re-reads the full JSON file).\n if (offset > 0 && result.meta?.sessionId && !result.absoluteIndices) {\n const existingCount = getTurnCount(result.meta.sessionId, source);\n if (existingCount > 0) {\n reindexTurns(result, existingCount);\n }\n // Re-index message ordinals for incremental reads\n if (result.messages.length > 0) {\n const maxOrd = getMaxOrdinal(result.meta.sessionId);\n reindexMessages(result, maxOrd + 1);\n }\n }\n\n if (!result.meta?.sessionId) {\n writeFileWatermark(filePath, result.newByteOffset);\n continue;\n }\n\n // Wrap all per-file DB writes in a single transaction so that\n // a crash can't leave messages inserted without watermark advancement\n // (which would cause tool_call duplication on retry).\n const sessionId = result.meta.sessionId;\n const fileMeta = result.meta;\n const fileResult = result;\n const db = getDb();\n (\n db.transaction(() => {\n upsertSession(fileMeta, filePath, source);\n\n if (fileResult.turns.length > 0) {\n insertTurns(fileResult.turns, source);\n updateSessionTotals(sessionId);\n }\n\n if (fileResult.events.length > 0) {\n insertScannerEvents(fileResult.events, source);\n }\n\n if (\n fileResult.messages.length > 0 ||\n fileResult.orphanedToolResults?.size\n ) {\n insertMessages(fileResult.messages, fileResult.orphanedToolResults);\n updateSessionMessageCounts(sessionId);\n }\n\n writeFileWatermark(filePath, fileResult.newByteOffset);\n }) as Database.Transaction\n )();\n\n newTurns += result.turns.length;\n\n // Process fork results (additional sessions from DAG analysis)\n if (result.forks) {\n for (const fork of result.forks) {\n if (!fork.meta?.sessionId) continue;\n const forkSessionId = fork.meta.sessionId;\n const forkMeta = fork.meta;\n (\n db.transaction(() => {\n upsertSession(forkMeta, filePath, source);\n if (fork.turns.length > 0) {\n insertTurns(fork.turns, source);\n updateSessionTotals(forkSessionId);\n }\n if (fork.events.length > 0) {\n insertScannerEvents(fork.events, source);\n }\n if (fork.messages.length > 0 || fork.orphanedToolResults?.size) {\n insertMessages(fork.messages, fork.orphanedToolResults);\n updateSessionMessageCounts(forkSessionId);\n }\n // No watermark — shared file, one watermark for the whole file\n }) as Database.Transaction\n )();\n newTurns += fork.turns.length;\n }\n }\n\n // Restore sync_ids after all data for this file has been re-inserted\n if (savedSyncIds) {\n restoreSyncIds(savedSyncIds);\n }\n\n // Archive raw file for 100% recall\n try {\n const fileSize = fs.statSync(filePath).size;\n const archivedSize = readArchivedSize(filePath);\n if (fileSize > archivedSize) {\n const rawContent = fs.readFileSync(filePath);\n getArchiveBackend().putSync(\n result.meta.sessionId,\n source,\n rawContent,\n );\n writeArchivedSize(filePath, fileSize);\n }\n } catch (archiveErr) {\n // Archive failure is non-fatal\n log.scanner.warn(\n `Archive error for ${filePath}: ${archiveErr instanceof Error ? archiveErr.message : archiveErr}`,\n );\n }\n }\n }\n\n // Link subagent sessions to parents after all files are processed\n if (filesScanned > 0) {\n const linked = linkSubagentSessions();\n if (linked > 0) {\n log.scanner.info(\n `Linked ${linked} subagent session${linked > 1 ? \"s\" : \"\"}`,\n );\n }\n log.scanner.info(`Scanned ${filesScanned} files, ${newTurns} new turns`);\n }\n\n return { filesScanned, newTurns };\n}\n\nfunction reindexTurns(result: ParseResult, startIndex: number): void {\n for (let i = 0; i < result.turns.length; i++) {\n result.turns[i].turnIndex = startIndex + i;\n }\n}\n\nfunction reindexMessages(result: ParseResult, startOrdinal: number): void {\n for (let i = 0; i < result.messages.length; i++) {\n result.messages[i].ordinal = startOrdinal + i;\n }\n}\n\nexport function createScannerLoop(opts: ScannerOptions): ScannerHandle {\n const idleMs = opts.idleIntervalMs ?? DEFAULT_IDLE_MS;\n const catchUpMs = opts.catchUpIntervalMs ?? DEFAULT_CATCHUP_MS;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n let stopping = false;\n let reparseChecked = false;\n let ready = false;\n\n function scheduleNext(hadWork: boolean): void {\n if (stopping) return;\n const delay = hadWork ? catchUpMs : idleMs;\n timer = setTimeout(() => tick(), delay);\n if (!opts.keepAlive && timer.unref) {\n timer.unref();\n }\n }\n\n function tick(): void {\n if (stopping) return;\n\n // On first tick, check if data version requires a full reparse\n if (!reparseChecked) {\n reparseChecked = true;\n if (needsResync()) {\n log.scanner.info(\"Data version outdated — running atomic reparse...\");\n import(\"./reparse.js\")\n .then(({ reparseAll }) => {\n try {\n const result = reparseAll((msg) => log.scanner.info(msg));\n if (result.success) {\n markResyncComplete();\n } else {\n log.scanner.error(\n `Reparse failed: ${result.error ?? \"unknown\"}`,\n );\n }\n } catch (err) {\n log.scanner.error(\n `Reparse error: ${err instanceof Error ? err.message : err}`,\n );\n }\n scheduleNext(true);\n })\n .catch((err) => {\n log.scanner.error(\n `Reparse import error: ${err instanceof Error ? err.message : err}`,\n );\n scheduleNext(false);\n });\n return;\n }\n // No reparse needed — stamp version if not already set\n markResyncComplete();\n }\n\n let hadWork = false;\n try {\n const { newTurns } = scanOnce();\n hadWork = newTurns > 0;\n\n if (!ready) {\n ready = true;\n opts.onReady?.();\n }\n\n // Only generate summaries when idle and scanner is ready.\n if (!hadWork && ready) {\n try {\n generateSummariesOnce((msg) => log.scanner.info(msg));\n } catch (err) {\n log.scanner.error(\n `Session summary error: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n } catch (err) {\n log.scanner.error(\n `Scan error: ${err instanceof Error ? err.message : err}`,\n );\n }\n if (!stopping) {\n scheduleNext(hadWork);\n }\n }\n\n return {\n start() {\n if (timer) return;\n stopping = false;\n log.scanner.info(\"Starting scanner\");\n tick();\n },\n stop() {\n stopping = true;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n log.scanner.info(\"Stopped scanner\");\n }\n },\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { gunzipSync, gzipSync } from \"node:zlib\";\nimport type { ArchiveBackend } from \"./backend.js\";\n\nexport class LocalArchiveBackend implements ArchiveBackend {\n constructor(private baseDir: string) {}\n\n putSync(sessionId: string, source: string, content: Buffer): void {\n const dir = path.join(this.baseDir, sessionId);\n fs.mkdirSync(dir, { recursive: true });\n const filePath = path.join(dir, `${source}.jsonl.gz`);\n const compressed = gzipSync(content);\n fs.writeFileSync(filePath, compressed);\n }\n\n getSync(sessionId: string, source: string): Buffer | null {\n const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);\n if (!fs.existsSync(filePath)) return null;\n const compressed = fs.readFileSync(filePath);\n return gunzipSync(compressed);\n }\n\n hasSync(sessionId: string, source: string): boolean {\n const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);\n return fs.existsSync(filePath);\n }\n\n list(): Array<{ sessionId: string; source: string; sizeBytes: number }> {\n const results: Array<{\n sessionId: string;\n source: string;\n sizeBytes: number;\n }> = [];\n\n if (!fs.existsSync(this.baseDir)) return results;\n\n for (const sessionId of fs.readdirSync(this.baseDir)) {\n const sessionDir = path.join(this.baseDir, sessionId);\n const stat = fs.statSync(sessionDir);\n if (!stat.isDirectory()) continue;\n\n for (const file of fs.readdirSync(sessionDir)) {\n if (!file.endsWith(\".jsonl.gz\")) continue;\n const source = file.replace(/\\.jsonl\\.gz$/, \"\");\n const fileStat = fs.statSync(path.join(sessionDir, file));\n results.push({\n sessionId,\n source,\n sizeBytes: fileStat.size,\n });\n }\n }\n\n return results;\n }\n\n stats(): { totalFiles: number; totalBytes: number } {\n const entries = this.list();\n return {\n totalFiles: entries.length,\n totalBytes: entries.reduce((sum, e) => sum + e.sizeBytes, 0),\n };\n }\n}\n","export type { ArchiveBackend } from \"./backend.js\";\nexport { LocalArchiveBackend } from \"./local.js\";\n\nimport path from \"node:path\";\nimport { config } from \"../config.js\";\nimport type { ArchiveBackend } from \"./backend.js\";\nimport { LocalArchiveBackend } from \"./local.js\";\n\nlet _backend: ArchiveBackend | null = null;\n\nexport function getArchiveBackend(): ArchiveBackend {\n if (!_backend) {\n _backend = new LocalArchiveBackend(path.join(config.dataDir, \"archive\"));\n }\n return _backend;\n}\n","import { execFileSync, spawnSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { log } from \"../log.js\";\n\nconst LLM_TIMEOUT_MS = 180_000;\n\nlet _claudePath: string | null | undefined;\n\n/**\n * Detect whether the `claude` CLI is available on this machine.\n * Result is cached for the lifetime of the process.\n */\nexport function detectAgent(): string | null {\n if (_claudePath !== undefined) return _claudePath;\n try {\n _claudePath = execFileSync(\"which\", [\"claude\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n _claudePath = null;\n }\n return _claudePath;\n}\n\n/**\n * Build a clean env that won't trigger recursive hooks or proxy loops.\n */\nfunction cleanEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [k, v] of Object.entries(process.env)) {\n if (v === undefined) continue;\n if (k === \"CLAUDECODE\") continue;\n if (k.startsWith(\"CLAUDE_CODE_\")) continue;\n if (k === \"ANTHROPIC_BASE_URL\") continue;\n env[k] = v;\n }\n return env;\n}\n\n/** Get the path to the panopticon MCP server script. */\nfunction getMcpServerPath(): string {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n // In the built dist/, summary code is in a chunk at dist/ level,\n // and mcp/server.js is at dist/mcp/server.js (same level)\n return path.resolve(dir, \"mcp\", \"server.js\");\n}\n\n/**\n * Invoke Claude CLI with a prompt and optional MCP server.\n * Returns the trimmed output text, or null on any failure.\n */\nexport function invokeLlm(\n prompt: string,\n opts: {\n timeoutMs?: number;\n withMcp?: boolean;\n systemPrompt?: string;\n model?: string;\n } = {},\n): string | null {\n const claudePath = detectAgent();\n if (!claudePath) return null;\n\n const timeoutMs = opts.timeoutMs ?? LLM_TIMEOUT_MS;\n\n const args = [\n claudePath,\n \"-p\",\n prompt,\n \"--output-format\",\n \"text\",\n \"--model\",\n opts.model ?? \"haiku\",\n \"--no-session-persistence\",\n \"--permission-mode\",\n \"auto\",\n ];\n\n if (opts.systemPrompt) {\n args.push(\"--append-system-prompt\", opts.systemPrompt);\n }\n\n if (opts.withMcp) {\n const mcpPath = getMcpServerPath();\n args.push(\n \"--strict-mcp-config\",\n \"--mcp-config\",\n JSON.stringify({\n mcpServers: {\n panopticon: {\n command: \"node\",\n args: [mcpPath],\n },\n },\n }),\n \"--allowed-tools\",\n \"mcp__panopticon__timeline\",\n \"mcp__panopticon__get\",\n \"mcp__panopticon__query\",\n \"mcp__panopticon__search\",\n \"mcp__panopticon__status\",\n );\n } else {\n args.push(\"--tools\", \"\");\n }\n\n const result = spawnSync(args[0], args.slice(1), {\n env: cleanEnv(),\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: timeoutMs,\n maxBuffer: 4 * 1024 * 1024,\n });\n\n const text = result.stdout?.toString().trim();\n const stderr = result.stderr?.toString().trim();\n\n if (stderr) log.llm.warn(`stderr: ${stderr.slice(0, 500)}`);\n if (result.signal) {\n log.llm.error(`killed by signal: ${result.signal}`);\n return null;\n }\n log.llm.info(`exit=${result.status} stdout=${text?.length ?? 0} chars`);\n\n // Accept output even with non-zero exit (hooks may cause exit code 1\n // after successful response)\n return text || null;\n}\n","import { getDb } from \"../db/schema.js\";\nimport { detectAgent, invokeLlm } from \"./llm.js\";\n\n/** Minimum messages before a session is worth summarizing. */\nconst MIN_MESSAGES = 3;\n/** Re-summarize when message count has grown by this much. */\nconst SUMMARY_THRESHOLD = 20;\n/** Max sessions to summarize per idle cycle. */\nconst MAX_PER_CYCLE = 50;\n/** Timeout for agent-based summary (longer than simple LLM call). */\nconst AGENT_TIMEOUT_MS = 120_000;\n\nconst SYSTEM_PROMPT = `You are summarizing a coding session for search and retrieval. You have access to panopticon MCP tools to explore the session data.\n\nInstructions:\n1. Use the \"timeline\" tool to read the session's messages and tool calls\n2. If needed, use \"get\" to read full message content or \"query\" for specific data\n3. Produce a summary optimized for AI consumption and full-text search\n4. Include: what was accomplished, key decisions made, specific file/function/package names, problems encountered and how they were resolved\n5. Use concrete names rather than generic descriptions (e.g. \"added FTS5 index on messages table\" not \"improved search\")\n6. Format as 2-4 concise sentences\n7. Output ONLY the summary text, nothing else`;\n\n/**\n * Generate a summary for a single session.\n * Uses Claude CLI with panopticon MCP if available, falls back to deterministic.\n */\nfunction _summarizeSession(\n sessionId: string,\n log: (msg: string) => void,\n): string | null {\n // Try agent-based summary first\n if (detectAgent()) {\n const prompt = `Summarize session ${sessionId}. Start by calling the timeline tool with sessionId \"${sessionId}\" and limit 50.`;\n const result = invokeLlm(prompt, {\n timeoutMs: AGENT_TIMEOUT_MS,\n withMcp: true,\n systemPrompt: SYSTEM_PROMPT,\n model: \"sonnet\",\n });\n if (result) return result;\n log(`LLM summary failed for ${sessionId}, falling back to deterministic`);\n }\n\n // Deterministic fallback\n return buildDeterministicSummary(sessionId);\n}\n\n/**\n * Build a deterministic summary from messages and tool_calls.\n */\nfunction buildDeterministicSummary(sessionId: string): string | null {\n const db = getDb();\n\n const firstUser = db\n .prepare(\n \"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\",\n )\n .get(sessionId) as { content: string } | undefined;\n\n const counts = db\n .prepare(\n \"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 = ?\",\n )\n .get(sessionId) as { msg_count: number; user_count: number };\n\n const tools = db\n .prepare(\n \"SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name ORDER BY cnt DESC LIMIT 5\",\n )\n .all(sessionId) as Array<{ tool_name: string; cnt: number }>;\n\n const files = db\n .prepare(\n \"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\",\n )\n .all(sessionId) as Array<{ fp: string | null }>;\n\n if (!firstUser && counts.msg_count === 0) return null;\n\n const parts: string[] = [];\n if (firstUser) parts.push(`Prompt: \"${firstUser.content}\"`);\n parts.push(`${counts.msg_count} messages (${counts.user_count} user)`);\n if (tools.length > 0) {\n parts.push(\n `Tools: ${tools.map((t) => `${t.tool_name}(${t.cnt})`).join(\", \")}`,\n );\n }\n const filePaths = files.map((f) => f.fp).filter(Boolean) as string[];\n if (filePaths.length > 0) {\n parts.push(`Files: ${filePaths.join(\", \")}`);\n }\n\n return parts.join(\". \");\n}\n\n/**\n * Generate summaries for sessions that need them.\n * Called when the scanner is idle.\n */\nexport function generateSummariesOnce(log: (msg: string) => void = () => {}): {\n updated: number;\n} {\n const db = getDb();\n let updated = 0;\n\n // Find sessions needing summary:\n // 1. Never summarized + enough messages\n // 2. Stale by message count (grown by THRESHOLD since last summary)\n // 3. Session ended after last summary\n const sessions = db\n .prepare(\n `\n SELECT s.session_id, s.message_count, s.summary_version, s.ended_at_ms,\n EXISTS(SELECT 1 FROM session_repositories WHERE session_id = s.session_id) as has_repo\n FROM sessions s\n WHERE s.message_count >= ?\n AND (\n s.summary IS NULL\n OR (s.message_count - COALESCE(s.summary_version, 0)) >= ?\n OR (s.ended_at_ms IS NOT NULL AND s.ended_at_ms > COALESCE(\n (SELECT MAX(created_at_ms) FROM session_summary_deltas WHERE session_id = s.session_id),\n 0\n ))\n )\n ORDER BY s.started_at_ms DESC\n LIMIT ?\n `,\n )\n .all(MIN_MESSAGES, SUMMARY_THRESHOLD, MAX_PER_CYCLE) as Array<{\n session_id: string;\n message_count: number;\n summary_version: number | null;\n ended_at_ms: number | null;\n has_repo: number;\n }>;\n\n for (const sess of sessions) {\n try {\n // TODO: re-enable LLM summaries once backfill is complete\n const summary = buildDeterministicSummary(sess.session_id);\n if (!summary) continue;\n\n db.prepare(\n \"UPDATE sessions SET summary = ?, summary_version = ?, sync_dirty = 1, sync_seq = COALESCE(sync_seq, 0) + 1 WHERE session_id = ?\",\n ).run(summary, sess.message_count, sess.session_id);\n\n updated++;\n log(`Summarized ${sess.session_id} (${sess.message_count} messages)`);\n } catch (err) {\n log(\n `Summary error for ${sess.session_id}: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n\n return { updated };\n}\n","import path from \"node:path\";\nimport { refreshIfStale } from \"../db/pricing.js\";\nimport { getDb } from \"../db/schema.js\";\nimport {\n upsertSessionCwd,\n upsertSessionRepository,\n upsertSession as upsertSessionRow,\n} from \"../db/store.js\";\nimport { resolveRepoFromCwd } from \"../repo.js\";\nimport type {\n ParsedEvent,\n ParsedMessage,\n ParsedSession,\n ParsedToolCall,\n ParsedTurn,\n} from \"../targets/types.js\";\n\n// ── Sync ID preservation across single-file reparses ──────────────────────\n\nexport interface SavedSyncIds {\n turns: Array<{\n sessionId: string;\n source: string;\n turnIndex: number;\n syncId: string;\n }>;\n events: Array<{\n sessionId: string;\n source: string;\n eventType: string;\n timestampMs: number;\n toolName: string;\n syncId: string;\n }>;\n toolCalls: Array<{\n sessionId: string;\n ordinal: number;\n toolUseId: string;\n toolName: string;\n syncId: string;\n }>;\n}\n\n// ── Session upsert (writes to unified sessions table) ───────────────────────\n\nexport function upsertSession(\n meta: ParsedSession,\n filePath: string,\n source: string,\n): void {\n // Derive project from repository or cwd basename\n let project: string | undefined;\n if (meta.cwd) {\n const info = resolveRepoFromCwd(meta.cwd);\n if (info) {\n project = info.repo; // e.g. \"fml-inc/panopticon\"\n } else {\n project = path.basename(meta.cwd);\n }\n }\n\n upsertSessionRow({\n session_id: meta.sessionId,\n target: source,\n started_at_ms: meta.startedAtMs,\n first_prompt: meta.firstPrompt,\n model: meta.model,\n cli_version: meta.cliVersion,\n scanner_file_path: filePath,\n has_scanner: 1,\n project,\n created_at: meta.startedAtMs ?? Date.now(),\n parent_session_id: meta.parentSessionId,\n relationship_type:\n meta.relationshipType ?? (meta.parentSessionId ? \"subagent\" : undefined),\n });\n\n // Record cwd and repo for scanner-only sessions\n if (meta.cwd) {\n upsertSessionCwd(meta.sessionId, meta.cwd, meta.startedAtMs ?? Date.now());\n }\n if (meta.cwd) {\n const info = resolveRepoFromCwd(meta.cwd);\n if (info) {\n upsertSessionRepository(\n meta.sessionId,\n info.repo,\n meta.startedAtMs ?? Date.now(),\n undefined,\n info.branch,\n );\n }\n }\n}\n\n// ── Turn insert ─────────────────────────────────────────────────────────────\n\nconst INSERT_TURN_SQL = `\n INSERT OR IGNORE INTO scanner_turns\n (session_id, source, turn_index, timestamp_ms, model, role,\n content_preview, input_tokens, output_tokens,\n cache_read_tokens, cache_creation_tokens, reasoning_tokens)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nexport function insertTurns(turns: ParsedTurn[], source: string): void {\n if (turns.length === 0) return;\n const db = getDb();\n const stmt = db.prepare(INSERT_TURN_SQL);\n const tx = db.transaction(() => {\n for (const t of turns) {\n stmt.run(\n t.sessionId,\n source,\n t.turnIndex,\n t.timestampMs,\n t.model ?? null,\n t.role,\n t.contentPreview ?? null,\n t.inputTokens,\n t.outputTokens,\n t.cacheReadTokens,\n t.cacheCreationTokens,\n t.reasoningTokens,\n );\n }\n });\n tx();\n}\n\n// ── Scanner events insert ───────────────────────────────────────────────────\n\nconst INSERT_EVENT_SQL = `\n INSERT OR IGNORE INTO scanner_events\n (session_id, source, event_type, timestamp_ms, tool_name, tool_input, tool_output, content, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nexport function insertScannerEvents(\n events: ParsedEvent[],\n source: string,\n): void {\n if (events.length === 0) return;\n const db = getDb();\n const stmt = db.prepare(INSERT_EVENT_SQL);\n const tx = db.transaction(() => {\n for (const e of events) {\n stmt.run(\n e.sessionId,\n source,\n e.eventType,\n e.timestampMs,\n e.toolName ?? null,\n e.toolInput ?? null,\n e.toolOutput ?? null,\n e.content ?? null,\n e.metadata ? JSON.stringify(e.metadata) : null,\n );\n }\n });\n tx();\n\n // Resolve repos from file paths in tool_call events (greedy attribution)\n const seen = new Set<string>();\n for (const e of events) {\n if (e.eventType !== \"tool_call\" || !e.toolInput) continue;\n try {\n const input = JSON.parse(e.toolInput);\n const fp = input.file_path ?? input.path;\n if (typeof fp !== \"string\" || !path.isAbsolute(fp)) continue;\n const dir = path.dirname(fp);\n if (seen.has(dir)) continue;\n seen.add(dir);\n const info = resolveRepoFromCwd(dir);\n if (info) {\n upsertSessionRepository(\n e.sessionId,\n info.repo,\n e.timestampMs,\n undefined,\n info.branch,\n );\n }\n } catch {\n // malformed tool_input JSON\n }\n }\n}\n\n// ── Session totals update (writes to unified sessions table) ────────────────\n\nconst UPDATE_TOTALS_SQL = `\n UPDATE sessions SET\n total_input_tokens = (SELECT COALESCE(SUM(input_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_output_tokens = (SELECT COALESCE(SUM(output_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_cache_read_tokens = (SELECT COALESCE(SUM(cache_read_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_cache_creation_tokens = (SELECT COALESCE(SUM(cache_creation_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_reasoning_tokens = (SELECT COALESCE(SUM(reasoning_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n turn_count = (SELECT COUNT(*) FROM scanner_turns WHERE session_id = ?)\n WHERE session_id = ?\n`;\n\nexport function updateSessionTotals(sessionId: string): void {\n const db = getDb();\n db.prepare(UPDATE_TOTALS_SQL).run(\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n );\n\n // Compute tool_counts from scanner tool_calls table\n const toolRows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name`,\n )\n .all(sessionId) as Array<{ tool_name: string; cnt: number }>;\n\n // Compute event_type_counts from scanner_events (strip \"progress:\" prefix)\n const eventRows = db\n .prepare(\n `SELECT event_type, COUNT(*) as cnt FROM scanner_events WHERE session_id = ? GROUP BY event_type`,\n )\n .all(sessionId) as Array<{ event_type: string; cnt: number }>;\n\n const toolCounts: Record<string, number> = {};\n for (const r of toolRows) toolCounts[r.tool_name] = r.cnt;\n\n const eventCounts: Record<string, number> = {};\n for (const r of eventRows) {\n const key = r.event_type.startsWith(\"progress:\")\n ? r.event_type.slice(\"progress:\".length)\n : r.event_type;\n eventCounts[key] = (eventCounts[key] ?? 0) + r.cnt;\n }\n\n if (toolRows.length > 0 || eventRows.length > 0) {\n db.prepare(\n `UPDATE sessions\n SET tool_counts = ?,\n event_type_counts = ?,\n sync_seq = COALESCE(sync_seq, 0) + 1\n WHERE session_id = ?`,\n ).run(JSON.stringify(toolCounts), JSON.stringify(eventCounts), sessionId);\n }\n // Scanner produces token data that needs pricing for cost queries\n refreshIfStale().catch(() => {});\n}\n\n// ── File watermarks ─────────────────────────────────────────────────────────\n\nexport function readFileWatermark(filePath: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT byte_offset FROM scanner_file_watermarks WHERE file_path = ?\",\n )\n .get(filePath) as { byte_offset: number } | undefined;\n return row?.byte_offset ?? 0;\n}\n\nexport function writeFileWatermark(filePath: string, byteOffset: number): void {\n const db = getDb();\n db.prepare(\n `INSERT INTO scanner_file_watermarks (file_path, byte_offset, last_scanned_ms)\n VALUES (?, ?, ?)\n ON CONFLICT(file_path) DO UPDATE SET byte_offset = excluded.byte_offset, last_scanned_ms = excluded.last_scanned_ms`,\n ).run(filePath, byteOffset, Date.now());\n}\n\n/**\n * Snapshot sync_id values for a session and its fork children so they\n * can be restored after re-insertion (preserving upstream sync identity).\n */\nfunction snapshotSyncIds(sessionId: string): SavedSyncIds {\n const db = getDb();\n const saved: SavedSyncIds = { turns: [], events: [], toolCalls: [] };\n\n const forkRows = db\n .prepare(\n \"SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\",\n )\n .all(sessionId) as Array<{ session_id: string }>;\n const allIds = [sessionId, ...forkRows.map((r) => r.session_id)];\n\n for (const sid of allIds) {\n const turns = db\n .prepare(\n \"SELECT session_id, source, turn_index, sync_id FROM scanner_turns WHERE session_id = ?\",\n )\n .all(sid) as Array<{\n session_id: string;\n source: string;\n turn_index: number;\n sync_id: string;\n }>;\n for (const t of turns) {\n saved.turns.push({\n sessionId: t.session_id,\n source: t.source,\n turnIndex: t.turn_index,\n syncId: t.sync_id,\n });\n }\n\n const events = db\n .prepare(\n \"SELECT session_id, source, event_type, timestamp_ms, COALESCE(tool_name, '') as tool_name, sync_id FROM scanner_events WHERE session_id = ?\",\n )\n .all(sid) as Array<{\n session_id: string;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string;\n sync_id: string;\n }>;\n for (const e of events) {\n saved.events.push({\n sessionId: e.session_id,\n source: e.source,\n eventType: e.event_type,\n timestampMs: e.timestamp_ms,\n toolName: e.tool_name,\n syncId: e.sync_id,\n });\n }\n\n const tcs = db\n .prepare(\n `SELECT tc.session_id, m.ordinal, COALESCE(tc.tool_use_id, '') as tool_use_id,\n tc.tool_name, tc.sync_id\n FROM tool_calls tc\n INNER JOIN messages m ON tc.message_id = m.id\n WHERE tc.session_id = ?`,\n )\n .all(sid) as Array<{\n session_id: string;\n ordinal: number;\n tool_use_id: string;\n tool_name: string;\n sync_id: string;\n }>;\n for (const tc of tcs) {\n saved.toolCalls.push({\n sessionId: tc.session_id,\n ordinal: tc.ordinal,\n toolUseId: tc.tool_use_id,\n toolName: tc.tool_name,\n syncId: tc.sync_id,\n });\n }\n }\n\n return saved;\n}\n\n/**\n * Reset a single file for full reparse: clear its watermark and delete\n * all turns, messages, tool_calls, and events for the session so the\n * full-file parse can re-insert cleanly (including fork detection).\n *\n * Returns previously-assigned sync_id values keyed by natural keys so\n * the caller can restore them after re-insertion.\n */\nexport function resetFileForReparse(\n filePath: string,\n sessionId?: string,\n): SavedSyncIds {\n const db = getDb();\n const saved: SavedSyncIds = sessionId\n ? snapshotSyncIds(sessionId)\n : { turns: [], events: [], toolCalls: [] };\n\n db.prepare(\"DELETE FROM scanner_file_watermarks WHERE file_path = ?\").run(\n filePath,\n );\n if (sessionId) {\n db.prepare(\"DELETE FROM scanner_turns WHERE session_id = ?\").run(sessionId);\n db.prepare(\"DELETE FROM scanner_events WHERE session_id = ?\").run(\n sessionId,\n );\n db.prepare(\"DELETE FROM tool_calls WHERE session_id = ?\").run(sessionId);\n db.prepare(\n \"DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id = ?)\",\n ).run(sessionId);\n db.prepare(\"DELETE FROM messages WHERE session_id = ?\").run(sessionId);\n // Also clean up any previously-detected fork sessions from this file\n const forkSessionFilter =\n \"SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\";\n db.prepare(\n `DELETE FROM scanner_turns WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM scanner_events WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM tool_calls WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id IN (${forkSessionFilter}))`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM messages WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n \"DELETE FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\",\n ).run(sessionId);\n }\n\n return saved;\n}\n\n/**\n * Restore previously-saved sync_id values after data has been re-inserted.\n * Matches rows by the same natural keys used in reparseAll().\n */\nexport function restoreSyncIds(saved: SavedSyncIds): void {\n if (!saved.turns.length && !saved.events.length && !saved.toolCalls.length)\n return;\n\n const db = getDb();\n const tx = db.transaction(() => {\n if (saved.turns.length > 0) {\n const stmt = db.prepare(\n \"UPDATE scanner_turns SET sync_id = ? WHERE session_id = ? AND source = ? AND turn_index = ?\",\n );\n for (const t of saved.turns) {\n stmt.run(t.syncId, t.sessionId, t.source, t.turnIndex);\n }\n }\n\n if (saved.events.length > 0) {\n const stmt = db.prepare(\n `UPDATE scanner_events SET sync_id = ?\n WHERE session_id = ? AND source = ? AND event_type = ? AND timestamp_ms = ?\n AND COALESCE(tool_name, '') = ?`,\n );\n for (const e of saved.events) {\n stmt.run(\n e.syncId,\n e.sessionId,\n e.source,\n e.eventType,\n e.timestampMs,\n e.toolName,\n );\n }\n }\n\n if (saved.toolCalls.length > 0) {\n const stmt = db.prepare(\n `UPDATE tool_calls SET sync_id = ?\n WHERE message_id IN (SELECT id FROM messages WHERE session_id = ? AND ordinal = ?)\n AND COALESCE(tool_use_id, '') = ?\n AND tool_name = ?`,\n );\n for (const tc of saved.toolCalls) {\n stmt.run(\n tc.syncId,\n tc.sessionId,\n tc.ordinal,\n tc.toolUseId,\n tc.toolName,\n );\n }\n }\n });\n tx();\n}\n\n// ── Turn count for incremental parsing ──────────────────────────────────────\n\nexport function getTurnCount(sessionId: string, source: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT COUNT(*) as count FROM scanner_turns WHERE session_id = ? AND source = ?\",\n )\n .get(sessionId, source) as { count: number };\n return row.count;\n}\n\n// ── Archive watermarks ─────────────────────────────────────────────────────\n\nexport function readArchivedSize(filePath: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT archived_size FROM scanner_file_watermarks WHERE file_path = ?\",\n )\n .get(filePath) as { archived_size: number } | undefined;\n return row?.archived_size ?? 0;\n}\n\nexport function writeArchivedSize(filePath: string, size: number): void {\n const db = getDb();\n db.prepare(\n \"UPDATE scanner_file_watermarks SET archived_size = ? WHERE file_path = ?\",\n ).run(size, filePath);\n}\n\n// ── Turn summaries ─────────────────────────────────────────────────────────\n\n// ── Messages & tool calls insert ───────────────────────────────────────────\n\n/** Build a short summary for tool-only assistant messages (no text content). */\nfunction toolUseSummary(toolCalls: ParsedToolCall[]): string {\n return toolCalls\n .map((tc) => {\n let label = \"\";\n if (tc.inputJson) {\n try {\n const input = JSON.parse(tc.inputJson);\n label =\n input.description ??\n input.command ??\n input.pattern ??\n input.file_path ??\n input.query ??\n input.prompt ??\n input.skill ??\n \"\";\n } catch {}\n }\n return label ? `[${tc.toolName}: ${label}]` : `[${tc.toolName}]`;\n })\n .join(\"\\n\");\n}\n\nconst INSERT_MESSAGE_SQL = `\n INSERT OR IGNORE INTO messages\n (session_id, ordinal, role, content, timestamp_ms,\n has_thinking, has_tool_use, content_length, is_system,\n model, token_usage, context_tokens, output_tokens,\n has_context_tokens, has_output_tokens, uuid, parent_uuid)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nconst INSERT_TOOL_CALL_SQL = `\n INSERT INTO tool_calls\n (message_id, session_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id, duration_ms)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\n/**\n * Insert parsed messages and their tool calls into the database.\n * Tool results from user messages are matched back to tool calls\n * from the preceding assistant message by tool_use_id.\n *\n * Also backfills tool_calls from previous scans whose result_content\n * was NULL because the tool_result arrived in a later batch.\n */\nexport function insertMessages(\n messages: ParsedMessage[],\n orphanedToolResults?: Map<\n string,\n { contentLength: number; contentRaw: string; timestampMs?: number }\n >,\n): void {\n if (messages.length === 0 && !orphanedToolResults?.size) return;\n const db = getDb();\n\n // Collect all tool results across user messages for backfilling\n const toolResultMap = new Map<\n string,\n { contentLength: number; contentRaw: string; timestampMs?: number }\n >();\n // Include orphaned results from filtered-out messages\n if (orphanedToolResults) {\n for (const [id, result] of orphanedToolResults) {\n toolResultMap.set(id, result);\n }\n }\n for (const msg of messages) {\n for (const [id, result] of msg.toolResults) {\n toolResultMap.set(id, result);\n }\n }\n\n const msgStmt = db.prepare(INSERT_MESSAGE_SQL);\n const tcStmt = db.prepare(INSERT_TOOL_CALL_SQL);\n const ftsStmt = db.prepare(\n \"INSERT INTO messages_fts(rowid, content) VALUES (?, ?)\",\n );\n\n const tx = db.transaction(() => {\n for (const msg of messages) {\n // Synthesize content for empty assistant messages with tool calls\n let content = msg.content;\n if (!content && msg.role === \"assistant\" && msg.toolCalls.length > 0) {\n content = toolUseSummary(msg.toolCalls);\n }\n\n const result = msgStmt.run(\n msg.sessionId,\n msg.ordinal,\n msg.role,\n content,\n msg.timestampMs ?? null,\n msg.hasThinking ? 1 : 0,\n msg.hasToolUse ? 1 : 0,\n msg.contentLength,\n msg.isSystem ? 1 : 0,\n msg.model ?? \"\",\n msg.tokenUsage ?? \"\",\n msg.contextTokens ?? 0,\n msg.outputTokens ?? 0,\n msg.hasContextTokens ? 1 : 0,\n msg.hasOutputTokens ? 1 : 0,\n msg.uuid ?? null,\n msg.parentUuid ?? null,\n );\n\n // INSERT OR IGNORE returns 0 changes if the row already exists\n if (result.changes === 0) continue;\n\n const messageId = result.lastInsertRowid;\n ftsStmt.run(messageId, content);\n\n for (const tc of msg.toolCalls) {\n // Look up result from the tool_result blocks\n const toolResult = toolResultMap.get(tc.toolUseId);\n const durationMs =\n tc.timestampMs && toolResult?.timestampMs\n ? toolResult.timestampMs - tc.timestampMs\n : null;\n tcStmt.run(\n messageId,\n msg.sessionId,\n tc.toolName,\n tc.category,\n tc.toolUseId,\n tc.inputJson ?? null,\n tc.skillName ?? null,\n toolResult?.contentLength ?? null,\n toolResult?.contentRaw ?? null,\n tc.subagentSessionId ?? null,\n durationMs != null && durationMs >= 0 ? durationMs : null,\n );\n }\n }\n\n // Backfill tool_calls from previous scans whose results arrived in this batch.\n if (toolResultMap.size > 0) {\n const backfillStmt = db.prepare(\n `UPDATE tool_calls\n SET result_content = ?, result_content_length = ?\n WHERE tool_use_id = ? AND result_content IS NULL`,\n );\n for (const [toolUseId, result] of toolResultMap) {\n backfillStmt.run(result.contentRaw, result.contentLength, toolUseId);\n }\n }\n });\n tx();\n}\n\n/**\n * Link subagent sessions to their parents.\n * Finds sessions whose ID appears in tool_calls.subagent_session_id\n * and sets their parent_session_id and relationship_type accordingly.\n */\nexport function linkSubagentSessions(): number {\n const db = getDb();\n // Only check sessions that don't already have a relationship set,\n // which limits work to newly-discovered sessions.\n const result = db\n .prepare(\n `UPDATE sessions\n SET parent_session_id = (\n SELECT tc.session_id\n FROM tool_calls tc\n WHERE tc.subagent_session_id = sessions.session_id\n LIMIT 1\n ),\n relationship_type = 'subagent',\n sync_seq = COALESCE(sync_seq, 0) + 1\n WHERE (relationship_type = '' OR relationship_type IS NULL)\n AND parent_session_id IS NULL\n AND EXISTS (\n SELECT 1 FROM tool_calls tc\n WHERE tc.subagent_session_id = sessions.session_id\n )`,\n )\n .run();\n return result.changes;\n}\n\n/**\n * Get the highest message ordinal for a session, or -1 if no messages exist.\n */\nexport function getMaxOrdinal(sessionId: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT MAX(ordinal) as max_ord FROM messages WHERE session_id = ?\",\n )\n .get(sessionId) as { max_ord: number | null };\n return row.max_ord ?? -1;\n}\n"],"mappings":"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","names":["fs","gunzipSync","fs","path","path","path","log","path","upsertSession","path","upsertSession","fs","reparseAll","fs","gunzipSync","log"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/sync/reader.ts","../src/sync/registry.ts","../src/sync/watermark.ts"],"sourcesContent":["import { getDb } from \"../db/schema.js\";\nimport type {\n HookEventRecord,\n MessageSyncRecord,\n MetricRow,\n OtelLogRecord,\n OtelSpanRecord,\n RepoConfigSnapshotRecord,\n ScannerEventRecord,\n ScannerTurnRecord,\n SessionSyncRecord,\n ToolCallSyncRecord,\n UserConfigSnapshotRecord,\n} from \"./types.js\";\n\nfunction parseJson(raw: string | null): Record<string, unknown> | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n return typeof parsed === \"object\" && parsed !== null ? parsed : null;\n } catch {\n return null;\n }\n}\n\n// ── Hook events ──────────────────────────────────────────────────────────────\n\nconst HOOK_EVENTS_SQL = `\n SELECT h.id, h.session_id, h.sync_id, h.event_type, h.timestamp_ms, h.cwd, h.repository,\n h.tool_name, decompress(h.payload) as payload,\n h.user_prompt, h.file_path, h.command, h.tool_result,\n s.target\n FROM hook_events h\n LEFT JOIN sessions s ON s.session_id = h.session_id\n WHERE h.id > ?\n ORDER BY h.id\n LIMIT ?\n`;\n\nexport function readHookEvents(\n afterId: number,\n limit: number,\n): { rows: HookEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(HOOK_EVENTS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n event_type: string;\n timestamp_ms: number;\n cwd: string | null;\n repository: string | null;\n tool_name: string | null;\n payload: string | null;\n user_prompt: string | null;\n file_path: string | null;\n command: string | null;\n tool_result: string | null;\n target: string | null;\n }>;\n\n const rows: HookEventRecord[] = rawRows.map((r) => ({\n hookId: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n cwd: r.cwd,\n repository: r.repository,\n toolName: r.tool_name,\n payload: parseJson(r.payload),\n userPrompt: r.user_prompt,\n filePath: r.file_path,\n command: r.command,\n toolResult: r.tool_result,\n target: r.target,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].hookId : afterId;\n return { rows, maxId };\n}\n\n// ── OTLP logs ────────────────────────────────────────────────────────────────\n\n/** Body types that hooks already cover — filtered out when hooks are installed. */\nconst HOOK_COVERED_BODIES = [\n // Claude Code\n \"claude_code.user_prompt\",\n \"claude_code.tool_decision\",\n \"claude_code.tool_result\",\n // Gemini CLI\n \"gemini_cli.user_prompt\",\n \"gemini_cli.tool_call\",\n \"gemini_cli.hook_call\",\n];\n\nconst ALL_LOGS_SQL = `\n SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\ninterface RawOtelLogRow {\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n body: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n severity_text: string | null;\n session_id: string | null;\n prompt_id: string | null;\n trace_id: string | null;\n span_id: string | null;\n}\n\nfunction mapOtelRows(rawRows: RawOtelLogRow[]): OtelLogRecord[] {\n return rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n body: r.body,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n severityText: r.severity_text,\n sessionId: r.session_id,\n promptId: r.prompt_id,\n traceId: r.trace_id,\n spanId: r.span_id,\n }));\n}\n\n/**\n * Read OTLP logs in batches.\n * When hooksInstalled is true, filters out body types that hooks cover\n * (tool_decision, tool_result, user_prompt) to avoid double-counting.\n */\n/**\n * Read OTLP logs in batches.\n * When hooksInstalled is true, filters out body types that hooks cover\n * (tool_decision, tool_result, user_prompt) to avoid double-counting.\n *\n * maxId advances to the highest id scanned (not just the highest returned),\n * so the watermark skips past blocks of filtered rows without stalling.\n */\nexport function readOtelLogs(\n afterId: number,\n limit: number,\n hooksInstalled: boolean,\n): { rows: OtelLogRecord[]; maxId: number } {\n const db = getDb();\n\n if (!hooksInstalled) {\n const rawRows = db\n .prepare(ALL_LOGS_SQL)\n .all(afterId, limit) as RawOtelLogRow[];\n const rows = mapOtelRows(rawRows);\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n }\n\n // When filtering, we need the max id from the unfiltered range so the\n // watermark advances past blocks of hook-covered rows.\n const scanMaxId = (\n db\n .prepare(\n \"SELECT MAX(id) as m FROM (SELECT id FROM otel_logs WHERE id > ? ORDER BY id LIMIT ?)\",\n )\n .get(afterId, limit) as { m: number | null }\n ).m;\n\n if (scanMaxId == null) return { rows: [], maxId: afterId };\n\n // Only return filtered rows within the scanned range (id <= scanMaxId),\n // not beyond it — otherwise we'd skip ahead of the scan window.\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE id > ? AND id <= ?\n AND body NOT IN (${HOOK_COVERED_BODIES.map(() => \"?\").join(\", \")})\n ORDER BY id`,\n )\n .all(afterId, scanMaxId, ...HOOK_COVERED_BODIES) as RawOtelLogRow[];\n\n const rows = mapOtelRows(rawRows);\n return { rows, maxId: scanMaxId };\n}\n\n// ── Metrics ──────────────────────────────────────────────────────────────────\n\nconst METRICS_SQL = `\n SELECT id, sync_id, timestamp_ns, name, value, metric_type, unit,\n attributes, resource_attributes, session_id\n FROM otel_metrics\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readMetrics(\n afterId: number,\n limit: number,\n): { rows: MetricRow[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(METRICS_SQL).all(afterId, limit) as Array<{\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n name: string;\n value: number;\n metric_type: string | null;\n unit: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: MetricRow[] = rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n name: r.name,\n value: r.value,\n metricType: r.metric_type,\n unit: r.unit,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Scanner turns ───────────────────────────────────────────────────────────\n\nconst SCANNER_TURNS_SQL = `\n SELECT t.id, t.session_id, t.sync_id, t.source, t.turn_index, t.timestamp_ms,\n t.model, t.role, t.content_preview,\n t.input_tokens, t.output_tokens, t.cache_read_tokens,\n t.cache_creation_tokens, t.reasoning_tokens,\n s.cli_version\n FROM scanner_turns t\n LEFT JOIN sessions s ON s.session_id = t.session_id\n WHERE t.id > ?\n ORDER BY t.id\n LIMIT ?\n`;\n\nexport function readScannerTurns(\n afterId: number,\n limit: number,\n): { rows: ScannerTurnRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SCANNER_TURNS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n turn_index: number;\n timestamp_ms: number;\n model: string | null;\n role: string | null;\n content_preview: string | null;\n input_tokens: number;\n output_tokens: number;\n cache_read_tokens: number;\n cache_creation_tokens: number;\n reasoning_tokens: number;\n cli_version: string | null;\n }>;\n\n const rows: ScannerTurnRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n turnIndex: r.turn_index,\n timestampMs: r.timestamp_ms,\n model: r.model,\n role: r.role,\n contentPreview: r.content_preview,\n inputTokens: r.input_tokens,\n outputTokens: r.output_tokens,\n cacheReadTokens: r.cache_read_tokens,\n cacheCreationTokens: r.cache_creation_tokens,\n reasoningTokens: r.reasoning_tokens,\n cliVersion: r.cli_version,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Scanner events ──────────────────────────────────────────────────────────\n\nconst SCANNER_EVENTS_SQL = `\n SELECT id, session_id, sync_id, source, event_type, timestamp_ms,\n tool_name, tool_input, tool_output, content, metadata\n FROM scanner_events\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readScannerEvents(\n afterId: number,\n limit: number,\n): { rows: ScannerEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SCANNER_EVENTS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string | null;\n tool_input: string | null;\n tool_output: string | null;\n content: string | null;\n metadata: string | null;\n }>;\n\n const rows: ScannerEventRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n toolName: r.tool_name,\n toolInput: r.tool_input,\n toolOutput: r.tool_output,\n content: r.content,\n metadata: parseJson(r.metadata),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── OTLP spans ─────────────────────────────────────────────────────────────\n\nconst SPANS_SQL = `\n SELECT id, trace_id, span_id, parent_span_id, name, kind,\n start_time_ns, end_time_ns, status_code, status_message,\n attributes, resource_attributes, session_id\n FROM otel_spans\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readOtelSpans(\n afterId: number,\n limit: number,\n): { rows: OtelSpanRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SPANS_SQL).all(afterId, limit) as Array<{\n id: number;\n trace_id: string;\n span_id: string;\n parent_span_id: string | null;\n name: string;\n kind: number | null;\n start_time_ns: number;\n end_time_ns: number;\n status_code: number | null;\n status_message: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: OtelSpanRecord[] = rawRows.map((r) => ({\n id: r.id,\n traceId: r.trace_id,\n spanId: r.span_id,\n parentSpanId: r.parent_span_id,\n name: r.name,\n kind: r.kind,\n startTimeNs: r.start_time_ns,\n endTimeNs: r.end_time_ns,\n statusCode: r.status_code,\n statusMessage: r.status_message,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── JSON parse helpers for config snapshots ─────────────────────────────────\n\nfunction parseJsonArray(raw: string | null): unknown[] {\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n}\n\nfunction parseJsonObject(raw: string | null): Record<string, unknown> {\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return typeof parsed === \"object\" &&\n parsed !== null &&\n !Array.isArray(parsed)\n ? parsed\n : {};\n } catch {\n return {};\n }\n}\n\n// ── User config snapshots ──────────────────────────────────────────────────\n\nconst USER_CONFIG_SQL = `\n SELECT id, device_name, snapshot_at_ms, content_hash,\n permissions, enabled_plugins, hooks, commands, rules, skills\n FROM user_config_snapshots\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readUserConfigSnapshots(\n afterId: number,\n limit: number,\n): { rows: UserConfigSnapshotRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(USER_CONFIG_SQL).all(afterId, limit) as Array<{\n id: number;\n device_name: string;\n snapshot_at_ms: number;\n content_hash: string;\n permissions: string | null;\n enabled_plugins: string | null;\n hooks: string | null;\n commands: string | null;\n rules: string | null;\n skills: string | null;\n }>;\n\n const rows: UserConfigSnapshotRecord[] = rawRows.map((r) => ({\n id: r.id,\n deviceName: r.device_name,\n snapshotAtMs: r.snapshot_at_ms,\n contentHash: r.content_hash,\n permissions: parseJsonObject(r.permissions),\n enabledPlugins: parseJsonArray(r.enabled_plugins),\n hooks: parseJsonArray(r.hooks),\n commands: parseJsonArray(r.commands),\n rules: parseJsonArray(r.rules),\n skills: parseJsonArray(r.skills),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Repo config snapshots ──────────────────────────────────────────────────\n\nconst REPO_CONFIG_SQL = `\n SELECT id, repository, cwd, session_id, snapshot_at_ms, content_hash,\n hooks, mcp_servers, commands, agents, rules,\n local_hooks, local_mcp_servers, local_permissions,\n local_is_gitignored, instructions\n FROM repo_config_snapshots\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readRepoConfigSnapshots(\n afterId: number,\n limit: number,\n): { rows: RepoConfigSnapshotRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(REPO_CONFIG_SQL).all(afterId, limit) as Array<{\n id: number;\n repository: string;\n cwd: string;\n session_id: string | null;\n snapshot_at_ms: number;\n content_hash: string;\n hooks: string | null;\n mcp_servers: string | null;\n commands: string | null;\n agents: string | null;\n rules: string | null;\n local_hooks: string | null;\n local_mcp_servers: string | null;\n local_permissions: string | null;\n local_is_gitignored: number;\n instructions: string | null;\n }>;\n\n const rows: RepoConfigSnapshotRecord[] = rawRows.map((r) => ({\n id: r.id,\n repository: r.repository,\n cwd: r.cwd,\n sessionId: r.session_id,\n snapshotAtMs: r.snapshot_at_ms,\n contentHash: r.content_hash,\n hooks: parseJsonArray(r.hooks),\n mcpServers: parseJsonArray(r.mcp_servers),\n commands: parseJsonArray(r.commands),\n agents: parseJsonArray(r.agents),\n rules: parseJsonArray(r.rules),\n localHooks: parseJsonArray(r.local_hooks),\n localMcpServers: parseJsonArray(r.local_mcp_servers),\n localPermissions: parseJsonObject(r.local_permissions),\n localIsGitignored: r.local_is_gitignored === 1,\n instructions: parseJsonArray(r.instructions),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Messages ────────────────────────────────────────────────────────────────\n\nconst MESSAGES_SQL = `\n SELECT id, session_id, ordinal, role, content, timestamp_ms,\n has_thinking, has_tool_use, content_length, is_system,\n model, token_usage, context_tokens, output_tokens,\n has_context_tokens, has_output_tokens\n FROM messages\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readMessages(\n afterId: number,\n limit: number,\n): { rows: MessageSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(MESSAGES_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n ordinal: number;\n role: string;\n content: string;\n timestamp_ms: number | null;\n has_thinking: number;\n has_tool_use: number;\n content_length: number;\n is_system: number;\n model: string;\n token_usage: string;\n context_tokens: number;\n output_tokens: number;\n has_context_tokens: number;\n has_output_tokens: number;\n }>;\n\n const rows: MessageSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n ordinal: r.ordinal,\n role: r.role,\n content: r.content,\n timestampMs: r.timestamp_ms,\n hasThinking: r.has_thinking === 1,\n hasToolUse: r.has_tool_use === 1,\n contentLength: r.content_length,\n isSystem: r.is_system === 1,\n model: r.model,\n tokenUsage: r.token_usage,\n contextTokens: r.context_tokens,\n outputTokens: r.output_tokens,\n hasContextTokens: r.has_context_tokens === 1,\n hasOutputTokens: r.has_output_tokens === 1,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Tool calls ──────────────────────────────────────────────────────────────\n\nconst TOOL_CALLS_SQL = `\n SELECT id, message_id, session_id, sync_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id\n FROM tool_calls\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readToolCalls(\n afterId: number,\n limit: number,\n): { rows: ToolCallSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(TOOL_CALLS_SQL).all(afterId, limit) as Array<{\n id: number;\n message_id: number;\n session_id: string;\n sync_id: string | null;\n tool_name: string;\n category: string;\n tool_use_id: string | null;\n input_json: string | null;\n skill_name: string | null;\n result_content_length: number | null;\n result_content: string | null;\n subagent_session_id: string | null;\n }>;\n\n const rows: ToolCallSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n messageId: r.message_id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n toolName: r.tool_name,\n category: r.category,\n toolUseId: r.tool_use_id,\n inputJson: r.input_json,\n skillName: r.skill_name,\n resultContentLength: r.result_content_length,\n resultContent: r.result_content,\n subagentSessionId: r.subagent_session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Sessions (watermark on sync_seq) ────────────────────────────────────────\n\nconst SESSIONS_SQL = `\n SELECT session_id, target, started_at_ms, ended_at_ms, cwd, first_prompt,\n permission_mode, agent_version,\n total_input_tokens, total_output_tokens, total_cache_read_tokens,\n total_cache_creation_tokens, total_reasoning_tokens, turn_count,\n models, summary, tool_counts, hook_tool_counts, event_type_counts, hook_event_type_counts, sync_seq,\n project, machine, message_count, user_message_count,\n parent_session_id, relationship_type, is_automated, created_at\n FROM sessions\n WHERE sync_seq > ?\n ORDER BY sync_seq\n LIMIT ?\n`;\n\nexport function readSessions(\n afterSeq: number,\n limit: number,\n): { rows: SessionSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SESSIONS_SQL).all(afterSeq, limit) as Array<{\n session_id: string;\n target: string | null;\n started_at_ms: number | null;\n ended_at_ms: number | null;\n cwd: string | null;\n first_prompt: string | null;\n permission_mode: string | null;\n agent_version: string | null;\n total_input_tokens: number | null;\n total_output_tokens: number | null;\n total_cache_read_tokens: number | null;\n total_cache_creation_tokens: number | null;\n total_reasoning_tokens: number | null;\n turn_count: number | null;\n models: string | null;\n summary: string | null;\n tool_counts: string | null;\n hook_tool_counts: string | null;\n event_type_counts: string | null;\n hook_event_type_counts: string | null;\n sync_seq: number;\n project: string | null;\n machine: string;\n message_count: number;\n user_message_count: number;\n parent_session_id: string | null;\n relationship_type: string;\n is_automated: number;\n created_at: number | null;\n }>;\n\n if (rawRows.length === 0) return { rows: [], maxId: afterSeq };\n\n const sessionIds = rawRows.map((r) => r.session_id);\n const placeholders = sessionIds.map(() => \"?\").join(\", \");\n\n const repoRows = db\n .prepare(\n `SELECT session_id, repository, first_seen_ms, git_user_name, git_user_email, branch\n FROM session_repositories\n WHERE session_id IN (${placeholders})`,\n )\n .all(...sessionIds) as Array<{\n session_id: string;\n repository: string;\n first_seen_ms: number;\n git_user_name: string | null;\n git_user_email: string | null;\n branch: string | null;\n }>;\n\n const cwdRows = db\n .prepare(\n `SELECT session_id, cwd, first_seen_ms\n FROM session_cwds\n WHERE session_id IN (${placeholders})`,\n )\n .all(...sessionIds) as Array<{\n session_id: string;\n cwd: string;\n first_seen_ms: number;\n }>;\n\n const reposBySession = new Map<string, SessionSyncRecord[\"repositories\"]>();\n for (const r of repoRows) {\n const list = reposBySession.get(r.session_id) ?? [];\n list.push({\n repository: r.repository,\n firstSeenMs: r.first_seen_ms,\n gitUserName: r.git_user_name,\n gitUserEmail: r.git_user_email,\n branch: r.branch,\n });\n reposBySession.set(r.session_id, list);\n }\n\n const cwdsBySession = new Map<string, SessionSyncRecord[\"cwds\"]>();\n for (const r of cwdRows) {\n const list = cwdsBySession.get(r.session_id) ?? [];\n list.push({ cwd: r.cwd, firstSeenMs: r.first_seen_ms });\n cwdsBySession.set(r.session_id, list);\n }\n\n const rows: SessionSyncRecord[] = rawRows.map((r) => ({\n sessionId: r.session_id,\n target: r.target,\n startedAtMs: r.started_at_ms,\n endedAtMs: r.ended_at_ms,\n cwd: r.cwd,\n firstPrompt: r.first_prompt,\n permissionMode: r.permission_mode,\n agentVersion: r.agent_version,\n totalInputTokens: r.total_input_tokens,\n totalOutputTokens: r.total_output_tokens,\n totalCacheReadTokens: r.total_cache_read_tokens,\n totalCacheCreationTokens: r.total_cache_creation_tokens,\n totalReasoningTokens: r.total_reasoning_tokens,\n turnCount: r.turn_count,\n models: r.models,\n summary: r.summary,\n toolCounts: parseJsonObject(r.tool_counts) as Record<string, number>,\n hookToolCounts: parseJsonObject(r.hook_tool_counts) as Record<\n string,\n number\n >,\n eventTypeCounts: parseJsonObject(r.event_type_counts) as Record<\n string,\n number\n >,\n hookEventTypeCounts: parseJsonObject(r.hook_event_type_counts) as Record<\n string,\n number\n >,\n project: r.project,\n machine: r.machine,\n messageCount: r.message_count,\n userMessageCount: r.user_message_count,\n parentSessionId: r.parent_session_id,\n relationshipType: r.relationship_type,\n isAutomated: r.is_automated === 1,\n createdAt: r.created_at,\n repositories: reposBySession.get(r.session_id) ?? [],\n cwds: cwdsBySession.get(r.session_id) ?? [],\n }));\n\n const maxId = rawRows[rawRows.length - 1].sync_seq;\n return { rows, maxId };\n}\n\n// ── Per-session readers (for gated sync) ──────────────────────────────────\n\nexport function readSessionMessages(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: MessageSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, session_id, ordinal, role, content, timestamp_ms,\n has_thinking, has_tool_use, content_length, is_system,\n model, token_usage, context_tokens, output_tokens,\n has_context_tokens, has_output_tokens\n FROM messages\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n ordinal: number;\n role: string;\n content: string;\n timestamp_ms: number | null;\n has_thinking: number;\n has_tool_use: number;\n content_length: number;\n is_system: number;\n model: string;\n token_usage: string;\n context_tokens: number;\n output_tokens: number;\n has_context_tokens: number;\n has_output_tokens: number;\n }>;\n\n const rows: MessageSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n ordinal: r.ordinal,\n role: r.role,\n content: r.content,\n timestampMs: r.timestamp_ms,\n hasThinking: r.has_thinking === 1,\n hasToolUse: r.has_tool_use === 1,\n contentLength: r.content_length,\n isSystem: r.is_system === 1,\n model: r.model,\n tokenUsage: r.token_usage,\n contextTokens: r.context_tokens,\n outputTokens: r.output_tokens,\n hasContextTokens: r.has_context_tokens === 1,\n hasOutputTokens: r.has_output_tokens === 1,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionToolCalls(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ToolCallSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, message_id, session_id, sync_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id\n FROM tool_calls\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n message_id: number;\n session_id: string;\n sync_id: string | null;\n tool_name: string;\n category: string;\n tool_use_id: string | null;\n input_json: string | null;\n skill_name: string | null;\n result_content_length: number | null;\n result_content: string | null;\n subagent_session_id: string | null;\n }>;\n\n const rows: ToolCallSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n messageId: r.message_id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n toolName: r.tool_name,\n category: r.category,\n toolUseId: r.tool_use_id,\n inputJson: r.input_json,\n skillName: r.skill_name,\n resultContentLength: r.result_content_length,\n resultContent: r.result_content,\n subagentSessionId: r.subagent_session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionScannerTurns(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ScannerTurnRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT t.id, t.session_id, t.sync_id, t.source, t.turn_index, t.timestamp_ms,\n t.model, t.role, t.content_preview,\n t.input_tokens, t.output_tokens, t.cache_read_tokens,\n t.cache_creation_tokens, t.reasoning_tokens,\n s.cli_version\n FROM scanner_turns t\n LEFT JOIN sessions s ON s.session_id = t.session_id\n WHERE t.session_id = ? AND t.id > ?\n ORDER BY t.id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n turn_index: number;\n timestamp_ms: number;\n model: string | null;\n role: string | null;\n content_preview: string | null;\n input_tokens: number;\n output_tokens: number;\n cache_read_tokens: number;\n cache_creation_tokens: number;\n reasoning_tokens: number;\n cli_version: string | null;\n }>;\n\n const rows: ScannerTurnRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n turnIndex: r.turn_index,\n timestampMs: r.timestamp_ms,\n model: r.model,\n role: r.role,\n contentPreview: r.content_preview,\n inputTokens: r.input_tokens,\n outputTokens: r.output_tokens,\n cacheReadTokens: r.cache_read_tokens,\n cacheCreationTokens: r.cache_creation_tokens,\n reasoningTokens: r.reasoning_tokens,\n cliVersion: r.cli_version,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionScannerEvents(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ScannerEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, session_id, sync_id, source, event_type, timestamp_ms,\n tool_name, tool_input, tool_output, content, metadata\n FROM scanner_events\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string | null;\n tool_input: string | null;\n tool_output: string | null;\n content: string | null;\n metadata: string | null;\n }>;\n\n const rows: ScannerEventRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n toolName: r.tool_name,\n toolInput: r.tool_input,\n toolOutput: r.tool_output,\n content: r.content,\n metadata: parseJson(r.metadata),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionHookEvents(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: HookEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT h.id, h.session_id, h.sync_id, h.event_type, h.timestamp_ms, h.cwd, h.repository,\n h.tool_name, decompress(h.payload) as payload,\n h.user_prompt, h.file_path, h.command, h.tool_result,\n s.target\n FROM hook_events h\n LEFT JOIN sessions s ON s.session_id = h.session_id\n WHERE h.session_id = ? AND h.id > ?\n ORDER BY h.id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n event_type: string;\n timestamp_ms: number;\n cwd: string | null;\n repository: string | null;\n tool_name: string | null;\n payload: string | null;\n user_prompt: string | null;\n file_path: string | null;\n command: string | null;\n tool_result: string | null;\n target: string | null;\n }>;\n\n const rows: HookEventRecord[] = rawRows.map((r) => ({\n hookId: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n cwd: r.cwd,\n repository: r.repository,\n toolName: r.tool_name,\n payload: parseJson(r.payload),\n userPrompt: r.user_prompt,\n filePath: r.file_path,\n command: r.command,\n toolResult: r.tool_result,\n target: r.target,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].hookId : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelLogs(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: OtelLogRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as RawOtelLogRow[];\n\n const rows = mapOtelRows(rawRows);\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelMetrics(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: MetricRow[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, name, value, metric_type, unit,\n attributes, resource_attributes, session_id\n FROM otel_metrics\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n name: string;\n value: number;\n metric_type: string | null;\n unit: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: MetricRow[] = rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n name: r.name,\n value: r.value,\n metricType: r.metric_type,\n unit: r.unit,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelSpans(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: OtelSpanRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, trace_id, span_id, parent_span_id, name, kind,\n start_time_ns, end_time_ns, status_code, status_message,\n attributes, resource_attributes, session_id\n FROM otel_spans\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n trace_id: string;\n span_id: string;\n parent_span_id: string | null;\n name: string;\n kind: number | null;\n start_time_ns: number;\n end_time_ns: number;\n status_code: number | null;\n status_message: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: OtelSpanRecord[] = rawRows.map((r) => ({\n id: r.id,\n traceId: r.trace_id,\n spanId: r.span_id,\n parentSpanId: r.parent_span_id,\n name: r.name,\n kind: r.kind,\n startTimeNs: r.start_time_ns,\n endTimeNs: r.end_time_ns,\n statusCode: r.status_code,\n statusMessage: r.status_message,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n/** Map of table name → per-session reader for gated sync. */\nexport const SESSION_READERS: Record<\n string,\n (\n sessionId: string,\n afterId: number,\n limit: number,\n ) => { rows: unknown[]; maxId: number }\n> = {\n messages: readSessionMessages,\n tool_calls: readSessionToolCalls,\n scanner_turns: readSessionScannerTurns,\n scanner_events: readSessionScannerEvents,\n hook_events: readSessionHookEvents,\n otel_logs: readSessionOtelLogs,\n otel_metrics: readSessionOtelMetrics,\n otel_spans: readSessionOtelSpans,\n};\n","import {\n readHookEvents,\n readMessages,\n readMetrics,\n readOtelLogs,\n readOtelSpans,\n readRepoConfigSnapshots,\n readScannerEvents,\n readScannerTurns,\n readSessions,\n readToolCalls,\n readUserConfigSnapshots,\n} from \"./reader.js\";\nimport type { TableSyncDescriptor } from \"./types.js\";\n\n/**\n * Ordered list of table sync descriptors. The order matches the\n * round-robin execution order in the sync loop.\n *\n * All tables sync via POST /v1/sync with {table, rows} payload.\n * Session-linked tables are filtered by repo attribution in the sync loop.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const TABLE_SYNC_REGISTRY: TableSyncDescriptor<any>[] = [\n // ── Session-linked tables (filtered by repo attribution) ─────────────────\n\n {\n table: \"sessions\",\n logNoun: \"sessions\",\n read: (afterId, limit) => readSessions(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"messages\",\n logNoun: \"messages\",\n read: (afterId, limit) => readMessages(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"tool_calls\",\n logNoun: \"tool calls\",\n read: (afterId, limit) => readToolCalls(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"scanner_turns\",\n logNoun: \"turns\",\n read: (afterId, limit) => readScannerTurns(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"scanner_events\",\n logNoun: \"events\",\n read: (afterId, limit) => readScannerEvents(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"hook_events\",\n logNoun: \"events\",\n read: (afterId, limit) => readHookEvents(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"otel_logs\",\n logNoun: \"logs\",\n read: (afterId, limit) => readOtelLogs(afterId, limit, false),\n sessionLinked: true,\n },\n\n {\n table: \"otel_metrics\",\n logNoun: \"metrics\",\n read: (afterId, limit) => readMetrics(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"otel_spans\",\n logNoun: \"spans\",\n read: (afterId, limit) => readOtelSpans(afterId, limit),\n sessionLinked: true,\n },\n\n // ── Non-session tables (always synced) ────────────────────────────────────\n\n {\n table: \"user_config_snapshots\",\n logNoun: \"snapshots\",\n read: (afterId, limit) => readUserConfigSnapshots(afterId, limit),\n sessionLinked: false,\n },\n\n {\n table: \"repo_config_snapshots\",\n logNoun: \"snapshots\",\n read: (afterId, limit) => readRepoConfigSnapshots(afterId, limit),\n sessionLinked: false,\n },\n];\n","import { getDb } from \"../db/schema.js\";\nimport { TABLE_SYNC_REGISTRY } from \"./registry.js\";\n\nexport function watermarkKey(table: string, targetName: string): string {\n return `${table}:${targetName}`;\n}\n\nexport function readWatermark(key: string): number {\n const db = getDb();\n const row = db\n .prepare(\"SELECT value FROM watermarks WHERE key = ?\")\n .get(key) as { value: number } | undefined;\n return row?.value ?? 0;\n}\n\nexport function writeWatermark(key: string, value: number): void {\n const db = getDb();\n db.prepare(\n \"INSERT INTO watermarks (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\",\n ).run(key, value);\n}\n\nexport function resetWatermarks(targetName?: string): void {\n const db = getDb();\n if (targetName) {\n const stmt = db.prepare(\"DELETE FROM watermarks WHERE key = ?\");\n for (const desc of TABLE_SYNC_REGISTRY) {\n stmt.run(watermarkKey(desc.table, targetName));\n }\n db.prepare(\"DELETE FROM target_session_sync WHERE target = ?\").run(\n targetName,\n );\n } else {\n db.prepare(\"DELETE FROM watermarks\").run();\n db.prepare(\"DELETE FROM target_session_sync\").run();\n }\n}\n"],"mappings":"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","names":["rawRows","rows"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server.ts","../src/api/routes.ts","../src/db/sync-prune.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport http from \"node:http\";\nimport { handleApiRequest } from \"./api/routes.js\";\nimport { config } from \"./config.js\";\nimport { autoPrune } from \"./db/prune.js\";\nimport { syncAwarePrune } from \"./db/sync-prune.js\";\nimport { type HookInput, processHookEvent } from \"./hooks/ingest.js\";\nimport { log } from \"./log.js\";\nimport { handleOtlpRequest } from \"./otlp/server.js\";\nimport { handleProxyRequest, tunnelWebSocket } from \"./proxy/server.js\";\nimport { createScannerLoop } from \"./scanner/index.js\";\nimport type { ScannerHandle } from \"./scanner/types.js\";\nimport {\n addBreadcrumb,\n captureException,\n flushSentry,\n initSentry,\n setTag,\n} from \"./sentry.js\";\nimport { createSyncLoop } from \"./sync/loop.js\";\nimport type { SyncHandle } from \"./sync/types.js\";\nimport { loadUnifiedConfig } from \"./unified-config.js\";\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nexport function createUnifiedServer(): http.Server {\n const server = http.createServer(async (req, res) => {\n const url = req.url ?? \"\";\n const method = req.method ?? \"\";\n\n // Health check\n if (url === \"/health\" && method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", port: config.port }));\n return;\n }\n\n // Hook event ingest\n if (url === \"/hooks\" && method === \"POST\") {\n try {\n const body = await collectBody(req);\n const data: HookInput = JSON.parse(body.toString(\"utf-8\"));\n addBreadcrumb(\"hooks\", `${data.hook_event_name ?? \"unknown\"} event`, {\n session_id: data.session_id,\n tool_name: data.tool_name,\n target: data.target ?? data.source,\n });\n const result = processHookEvent(data);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n log.hooks.error(\"Hook ingest error:\", err);\n captureException(err, { component: \"hooks\" });\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"hook ingest failed\" }));\n }\n }\n return;\n }\n\n // OTLP ingest — /v1/logs, /v1/metrics, /v1/traces, or bare \"/\" (Gemini)\n if (\n method === \"POST\" &&\n (url.startsWith(\"/v1/\") || url === \"/\" || url === \"\")\n ) {\n await handleOtlpRequest(req, res);\n return;\n }\n\n // Proxy routes — /proxy/anthropic/*, /proxy/openai/*, /proxy/codex/*, /proxy/google/*\n if (url.startsWith(\"/proxy/\")) {\n if (method !== \"POST\") {\n res.writeHead(405);\n res.end();\n return;\n }\n addBreadcrumb(\"proxy\", `Proxy ${url}`);\n // Strip /proxy prefix so the proxy handler sees /anthropic/*, /openai/*, etc.\n req.url = url.slice(6);\n await handleProxyRequest(req, res);\n return;\n }\n\n // API routes — /api/tool, /api/exec\n if (url.startsWith(\"/api/\") && method === \"POST\") {\n await handleApiRequest(req, res);\n return;\n }\n\n // 404\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"not_found\" }));\n });\n\n // WebSocket upgrades for proxy routes\n server.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n if (url.startsWith(\"/proxy/\")) {\n req.url = url.slice(6);\n tunnelWebSocket(req, socket, head);\n } else {\n socket.end(\"HTTP/1.1 404 Not Found\\r\\n\\r\\n\");\n }\n });\n\n return server;\n}\n\n// When run directly, start the unified server\nconst entryScript = process.argv[1]?.replaceAll(\"\\\\\", \"/\") ?? \"\";\nif (entryScript.endsWith(\"/server.js\") || entryScript.endsWith(\"/server.ts\")) {\n const PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\n function runPrune(): void {\n try {\n const cfg = loadUnifiedConfig();\n addBreadcrumb(\"prune\", \"Running scheduled prune\");\n autoPrune(cfg.retention.maxAgeDays, cfg.retention.maxSizeMb);\n if (cfg.sync.targets.length > 0 && cfg.retention.syncedMaxAgeDays) {\n syncAwarePrune(cfg.sync.targets, cfg.retention);\n }\n } catch (err) {\n log.server.error(\"Prune error:\", err);\n captureException(err, { component: \"prune\" });\n }\n }\n\n const sentryActive = initSentry();\n if (sentryActive) log.server.info(\"Sentry: enabled\");\n\n const server = createUnifiedServer();\n let syncHandle: SyncHandle | null = null;\n let scannerHandle: ScannerHandle | null = null;\n let pruneTimer: ReturnType<typeof setInterval> | null = null;\n\n let takeoverAttempted = false;\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n if (takeoverAttempted) {\n log.server.warn(`Already running on ${config.host}:${config.port}`);\n process.exit(0);\n }\n takeoverAttempted = true;\n log.server.warn(`Port ${config.port} in use, attempting takeover...`);\n try {\n // Only kill the old panopticon server via PID file, not all\n // processes on the port (which could include Claude Code CLI)\n const pidFile = config.serverPidFile;\n const pidStr = fs.readFileSync(pidFile, \"utf-8\").trim();\n const oldPid = parseInt(pidStr, 10);\n if (oldPid && oldPid !== process.pid) {\n try {\n process.kill(oldPid, \"SIGTERM\");\n } catch {}\n }\n setTimeout(() => server.listen(config.port, config.host), 1500);\n } catch {\n log.server.warn(`Already running on ${config.host}:${config.port}`);\n process.exit(0);\n }\n return;\n }\n captureException(err, { component: \"server\" });\n throw err;\n });\n server.listen(config.port, config.host, () => {\n log.server.info(`Listening on ${config.host}:${config.port}`);\n\n const cfg = loadUnifiedConfig();\n\n // Start session file scanner first — sync is deferred until scanner\n // finishes any initial resync so we don't sync stale/partial data.\n scannerHandle = createScannerLoop({\n onReady: () => {\n if (cfg.sync.targets.length > 0) {\n log.sync.info(\n `Targets: ${cfg.sync.targets.map((t) => t.name).join(\", \")}`,\n );\n setTag(\"sync_targets\", cfg.sync.targets.length);\n syncHandle = createSyncLoop({\n targets: cfg.sync.targets,\n filter: cfg.sync.filter,\n });\n syncHandle.start();\n }\n },\n });\n scannerHandle.start();\n\n // Run prune on startup, then hourly\n runPrune();\n pruneTimer = setInterval(runPrune, PRUNE_INTERVAL_MS);\n pruneTimer.unref();\n });\n\n const shutdown = async () => {\n if (pruneTimer) clearInterval(pruneTimer);\n scannerHandle?.stop();\n syncHandle?.stop();\n await flushSentry();\n server.close();\n process.exit(0);\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGHUP\", shutdown);\n}\n","/**\n * Server-side API route handler.\n *\n * Two endpoints:\n * POST /api/tool — read-only query dispatch (CLI + MCP)\n * POST /api/exec — write command dispatch (CLI only)\n */\nimport type http from \"node:http\";\nimport { refreshPricing } from \"../db/pricing.js\";\nimport { pruneEstimate, pruneExecute } from \"../db/prune.js\";\nimport {\n activitySummary,\n costBreakdown,\n dbStats,\n listPlans,\n listSessions,\n print,\n rawQuery,\n search,\n sessionTimeline,\n} from \"../db/query.js\";\nimport { getDb } from \"../db/schema.js\";\nimport { log } from \"../log.js\";\nimport { addTarget, listTargets, removeTarget } from \"../sync/config.js\";\nimport { TABLE_SYNC_REGISTRY } from \"../sync/registry.js\";\nimport type { SyncTarget } from \"../sync/types.js\";\nimport {\n readWatermark,\n resetWatermarks,\n watermarkKey,\n writeWatermark,\n} from \"../sync/watermark.js\";\n\n// ── Tool dispatch ────────────────────────────────────────────────────────────\n\ntype ToolFn = (params: Record<string, unknown>) => unknown;\n\nconst TOOLS: Record<string, ToolFn> = {\n sessions: (p) => listSessions(p as Parameters<typeof listSessions>[0]),\n timeline: (p) => sessionTimeline(p as Parameters<typeof sessionTimeline>[0]),\n costs: (p) => costBreakdown(p as Parameters<typeof costBreakdown>[0]),\n summary: (p) => activitySummary(p as Parameters<typeof activitySummary>[0]),\n plans: (p) => listPlans(p as Parameters<typeof listPlans>[0]),\n search: (p) => search(p as Parameters<typeof search>[0]),\n get: (p) => print(p as Parameters<typeof print>[0]),\n query: (p) => rawQuery((p as { sql: string }).sql),\n status: () => dbStats(),\n};\n\n// ── Exec dispatch ────────────────────────────────────────────────────────────\n\ntype ExecFn = (params: Record<string, unknown>) => unknown;\n\nconst EXEC: Record<string, ExecFn> = {\n prune: (p) => {\n const cutoffMs = p.cutoffMs as number;\n if (typeof cutoffMs !== \"number\") {\n throw new Error(\"cutoffMs is required and must be a number\");\n }\n if (p.dryRun) {\n return pruneEstimate(cutoffMs);\n }\n const result = pruneExecute(cutoffMs);\n if (p.vacuum) {\n const db = getDb();\n db.pragma(\"wal_checkpoint(TRUNCATE)\");\n db.exec(\"VACUUM\");\n }\n return result;\n },\n \"refresh-pricing\": () => refreshPricing(),\n \"sync-reset\": (p) => {\n const target = p.target as string | undefined;\n resetWatermarks(target);\n return { ok: true, target: target ?? \"all\" };\n },\n \"sync-watermark-get\": (p) => {\n const target = p.target as string;\n if (!target) throw new Error(\"target is required\");\n const table = p.table as string | undefined;\n if (table) {\n return {\n key: watermarkKey(table, target),\n value: readWatermark(watermarkKey(table, target)),\n };\n }\n // Return all watermarks for this target\n const watermarks: Record<string, number> = {};\n for (const desc of TABLE_SYNC_REGISTRY) {\n const key = watermarkKey(desc.table, target);\n watermarks[desc.table] = readWatermark(key);\n }\n return { target, watermarks };\n },\n \"sync-watermark-set\": (p) => {\n const target = p.target as string;\n const table = p.table as string;\n const value = p.value as number;\n if (!target) throw new Error(\"target is required\");\n if (!table) throw new Error(\"table is required\");\n if (typeof value !== \"number\") throw new Error(\"value must be a number\");\n const key = watermarkKey(table, target);\n writeWatermark(key, value);\n return { key, value };\n },\n \"sync-pending\": (p) => {\n const target = p.target as string;\n if (!target) throw new Error(\"target is required\");\n const db = getDb();\n const pending: Record<\n string,\n { maxId: number; watermark: number; pending: number }\n > = {};\n for (const desc of TABLE_SYNC_REGISTRY) {\n const key = watermarkKey(desc.table, target);\n const wm = readWatermark(key);\n const maxId =\n (\n db\n .prepare(\n `SELECT MAX(${desc.table === \"sessions\" ? \"sync_seq\" : \"id\"}) as m FROM ${desc.table}`,\n )\n .get() as {\n m: number | null;\n }\n )?.m ?? 0;\n const count = Math.max(0, maxId - wm);\n if (count > 0) {\n pending[desc.table] = { maxId, watermark: wm, pending: count };\n }\n }\n const totalPending = Object.values(pending).reduce(\n (s, v) => s + v.pending,\n 0,\n );\n return { target, totalPending, tables: pending };\n },\n \"sync-target-list\": () => {\n return { targets: listTargets() };\n },\n \"sync-target-add\": (p) => {\n const target = p as unknown as SyncTarget;\n if (!target.name) throw new Error(\"name is required\");\n if (!target.url) throw new Error(\"url is required\");\n addTarget(target);\n return { ok: true, name: target.name, url: target.url };\n },\n \"sync-target-remove\": (p) => {\n const name = p.name as string;\n if (!name) throw new Error(\"name is required\");\n const removed = removeTarget(name);\n return { ok: removed, name };\n },\n};\n\n// ── Request handler ──────────────────────────────────────────────────────────\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction jsonResponse(\n res: http.ServerResponse,\n status: number,\n data: unknown,\n): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n}\n\nexport async function handleApiRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n): Promise<void> {\n const url = req.url ?? \"\";\n\n let body: Record<string, unknown>;\n try {\n const raw = await collectBody(req);\n body = raw.length > 0 ? JSON.parse(raw.toString(\"utf-8\")) : {};\n } catch {\n jsonResponse(res, 400, { error: \"Invalid JSON body\" });\n return;\n }\n\n if (url === \"/api/tool\") {\n const name = body.name as string | undefined;\n if (!name || !(name in TOOLS)) {\n jsonResponse(res, 404, {\n error: `Unknown tool: ${name}`,\n available: Object.keys(TOOLS),\n });\n return;\n }\n try {\n const params = (body.params as Record<string, unknown>) ?? {};\n const result = TOOLS[name](params);\n jsonResponse(res, 200, result);\n } catch (err) {\n log.server.error(`API tool \"${name}\" error:`, err);\n jsonResponse(res, 500, {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n\n if (url === \"/api/exec\") {\n const command = body.command as string | undefined;\n if (!command || !(command in EXEC)) {\n jsonResponse(res, 404, {\n error: `Unknown command: ${command}`,\n available: Object.keys(EXEC),\n });\n return;\n }\n try {\n const params = (body.params as Record<string, unknown>) ?? {};\n const result = await EXEC[command](params);\n jsonResponse(res, 200, result ?? { ok: true });\n } catch (err) {\n log.server.error(`API exec \"${command}\" error:`, err);\n jsonResponse(res, 500, {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n\n jsonResponse(res, 404, { error: \"Unknown API endpoint\", url });\n}\n","import type { SyncTarget } from \"../sync/types.js\";\nimport { readWatermark, watermarkKey } from \"../sync/watermark.js\";\nimport type { RetentionConfig } from \"../unified-config.js\";\nimport { getDb } from \"./schema.js\";\n\nexport interface SyncPruneResult {\n hook_events: number;\n otel_logs: number;\n otel_metrics: number;\n}\n\n/**\n * Compute the minimum watermark for a table across all sync targets.\n * Returns 0 if no targets exist or any target has watermark 0 (hasn't synced yet).\n */\nexport function minWatermarkForTable(\n table: string,\n targets: SyncTarget[],\n): number {\n if (targets.length === 0) return 0;\n\n let min = Infinity;\n for (const t of targets) {\n const wm = readWatermark(watermarkKey(table, t.name));\n if (wm === 0) return 0;\n if (wm < min) min = wm;\n }\n return min === Infinity ? 0 : min;\n}\n\n/**\n * Aggressively prune rows that have been confirmed synced to ALL targets\n * and are older than `syncedMaxAgeDays`.\n *\n * No-op when:\n * - No sync targets configured\n * - `syncedMaxAgeDays` is not set\n * - Any target has watermark 0 for a given table (hasn't completed first sync)\n */\nexport function syncAwarePrune(\n targets: SyncTarget[],\n retention: RetentionConfig,\n): SyncPruneResult {\n const result: SyncPruneResult = {\n hook_events: 0,\n otel_logs: 0,\n otel_metrics: 0,\n };\n\n if (!retention.syncedMaxAgeDays || targets.length === 0) {\n return result;\n }\n\n const cutoffMs =\n Date.now() - retention.syncedMaxAgeDays * 24 * 60 * 60 * 1000;\n const cutoffNs = cutoffMs * 1_000_000;\n const db = getDb();\n\n const tx = db.transaction(() => {\n // -- hook_events --\n const hookMinWm = minWatermarkForTable(\"hook_events\", targets);\n if (hookMinWm > 0) {\n db.prepare(\n \"DELETE FROM hook_events_fts WHERE rowid IN (SELECT id FROM hook_events WHERE id <= ? AND timestamp_ms < ?)\",\n ).run(hookMinWm, cutoffMs);\n\n result.hook_events = db\n .prepare(\"DELETE FROM hook_events WHERE id <= ? AND timestamp_ms < ?\")\n .run(hookMinWm, cutoffMs).changes;\n }\n\n // -- otel_logs --\n const logsMinWm = minWatermarkForTable(\"otel_logs\", targets);\n if (logsMinWm > 0) {\n result.otel_logs = db\n .prepare(\"DELETE FROM otel_logs WHERE id <= ? AND timestamp_ns < ?\")\n .run(logsMinWm, cutoffNs).changes;\n }\n\n // -- otel_metrics --\n const metricsMinWm = minWatermarkForTable(\"otel_metrics\", targets);\n if (metricsMinWm > 0) {\n result.otel_metrics = db\n .prepare(\"DELETE FROM otel_metrics WHERE id <= ? AND timestamp_ns < ?\")\n .run(metricsMinWm, cutoffNs).changes;\n }\n\n // Session metadata (session_repositories, session_cwds) is local-only —\n // not synced to remote targets. Left to regular time/size-based pruneExecute.\n });\n\n tx();\n return result;\n}\n"],"mappings":"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","names":["collectBody","runPrune"]}