@tekmidian/pai 0.66.6 → 0.66.7

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 (118) hide show
  1. package/dist/{aibroker-client-WCZi_tn-.mjs → aibroker-client-BevXY1Zz.mjs} +3 -3
  2. package/dist/{aibroker-client-WCZi_tn-.mjs.map → aibroker-client-BevXY1Zz.mjs.map} +1 -1
  3. package/dist/{aibroker-client-Ck3p3k_-.mjs → aibroker-client-CxTpL_u7.mjs} +1 -1
  4. package/dist/{auto-route-BnizyALK.mjs → auto-route-BVeAp0fl.mjs} +1 -1
  5. package/dist/{auto-route-BnizyALK.mjs.map → auto-route-BVeAp0fl.mjs.map} +1 -1
  6. package/dist/{auto-route-D6fW1Q8z.mjs → auto-route-CboBqsN3.mjs} +1 -1
  7. package/dist/{chain-2_uoiEdD.mjs → chain-8hFKr23C.mjs} +3 -3
  8. package/dist/{chain-2_uoiEdD.mjs.map → chain-8hFKr23C.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +16 -15
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +15 -14
  12. package/dist/{config-CnsDMETf.mjs → config-UWtzLpXi.mjs} +1 -1
  13. package/dist/{config-Ddc4DIPk.mjs → config-sfaArCsl.mjs} +12 -3
  14. package/dist/config-sfaArCsl.mjs.map +1 -0
  15. package/dist/{context-handover-cache-SFkWucuT.mjs → context-handover-cache-Bb-H_mTa.mjs} +7 -8
  16. package/dist/{context-handover-cache-SFkWucuT.mjs.map → context-handover-cache-Bb-H_mTa.mjs.map} +1 -1
  17. package/dist/daemon/index.mjs +14 -13
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/daemon-BXTn2EgV.mjs +19 -0
  20. package/dist/{daemon-vLtT3yaI.mjs → daemon-CkbX-hV7.mjs} +757 -20
  21. package/dist/daemon-CkbX-hV7.mjs.map +1 -0
  22. package/dist/daemon-mcp/index.mjs +12 -11
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/embedding-gate-BVrr0BgS.mjs +378 -0
  25. package/dist/embedding-gate-BVrr0BgS.mjs.map +1 -0
  26. package/dist/embedding-gate-Crj8awk7.mjs +5 -0
  27. package/dist/embeddings-BuA7v4lk.mjs +3 -0
  28. package/dist/{embeddings-DMVzpvRZ.mjs → embeddings-DFl88_y-.mjs} +16 -2
  29. package/dist/{embeddings-DMVzpvRZ.mjs.map → embeddings-DFl88_y-.mjs.map} +1 -1
  30. package/dist/{env-DiolswKQ.mjs → env-DqTjeKoA.mjs} +1 -1
  31. package/dist/{env-DiolswKQ.mjs.map → env-DqTjeKoA.mjs.map} +1 -1
  32. package/dist/factory-XrH66erA.mjs +9 -0
  33. package/dist/{factory-CXFHwvcv.mjs → factory-mNBitkL_.mjs} +28 -6
  34. package/dist/factory-mNBitkL_.mjs.map +1 -0
  35. package/dist/{fallback-DNzd3jJc.mjs → fallback-DdC1_s2d.mjs} +4 -4
  36. package/dist/{fallback-DNzd3jJc.mjs.map → fallback-DdC1_s2d.mjs.map} +1 -1
  37. package/dist/hooks/block-sleep-poll.mjs +7 -2
  38. package/dist/hooks/block-sleep-poll.mjs.map +3 -3
  39. package/dist/hooks/context-compression-hook.mjs +2 -0
  40. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  41. package/dist/hooks/load-core-context.mjs +5 -5
  42. package/dist/hooks/load-core-context.mjs.map +2 -2
  43. package/dist/hooks/load-project-context.mjs +2 -0
  44. package/dist/hooks/load-project-context.mjs.map +2 -2
  45. package/dist/hooks/post-compact-inject.mjs +2 -0
  46. package/dist/hooks/post-compact-inject.mjs.map +2 -2
  47. package/dist/hooks/route-agents-to-worker.mjs +2 -0
  48. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  49. package/dist/hooks/security-validator.mjs +2 -0
  50. package/dist/hooks/security-validator.mjs.map +2 -2
  51. package/dist/hooks/whisper-rules.mjs +2 -0
  52. package/dist/hooks/whisper-rules.mjs.map +2 -2
  53. package/dist/hooks/worker-guard.mjs +82 -5
  54. package/dist/hooks/worker-guard.mjs.map +4 -4
  55. package/dist/hooks/worker-proxy.mjs +2 -0
  56. package/dist/hooks/worker-proxy.mjs.map +2 -2
  57. package/dist/hooks/worker-status-line.mjs +7 -2
  58. package/dist/hooks/worker-status-line.mjs.map +3 -3
  59. package/dist/hooks/worker-supervision.mjs +2 -0
  60. package/dist/hooks/worker-supervision.mjs.map +2 -2
  61. package/dist/{ipc-client-DkxRay-o.mjs → ipc-client-BgSzY9QJ.mjs} +2 -2
  62. package/dist/{ipc-client-DkxRay-o.mjs.map → ipc-client-BgSzY9QJ.mjs.map} +1 -1
  63. package/dist/main-resolver-CobQGm2B.mjs +7 -0
  64. package/dist/{main-resolver-SfGGg1DB.mjs → main-resolver-DEXky_Km.mjs} +6 -6
  65. package/dist/{main-resolver-SfGGg1DB.mjs.map → main-resolver-DEXky_Km.mjs.map} +1 -1
  66. package/dist/{planner-C-gzg7ye.mjs → planner-DBoB5GtC.mjs} +10 -9
  67. package/dist/{planner-C-gzg7ye.mjs.map → planner-DBoB5GtC.mjs.map} +1 -1
  68. package/dist/{postgres-BxmyQHoA.mjs → postgres-D-96lgE1.mjs} +84 -3
  69. package/dist/postgres-D-96lgE1.mjs.map +1 -0
  70. package/dist/postgres-TedJP3yF.mjs +7 -0
  71. package/dist/{program-CfG1T_ko.mjs → program-BXns_5kO.mjs} +211 -63
  72. package/dist/program-BXns_5kO.mjs.map +1 -0
  73. package/dist/{query-feedback-BPdapXKd.mjs → query-feedback-VI7Cl-Lr.mjs} +2 -2
  74. package/dist/{query-feedback-BPdapXKd.mjs.map → query-feedback-VI7Cl-Lr.mjs.map} +1 -1
  75. package/dist/router-CKn9e0fo.mjs +3 -0
  76. package/dist/{router-BaTbc9VX.mjs → router-dfjlaOMI.mjs} +2 -2
  77. package/dist/{router-BaTbc9VX.mjs.map → router-dfjlaOMI.mjs.map} +1 -1
  78. package/dist/{run-ChlIbDJ0.mjs → run-DPwQkRwh.mjs} +107 -9
  79. package/dist/run-DPwQkRwh.mjs.map +1 -0
  80. package/dist/{run-env-CQRnxNht.mjs → run-env-CkAmx3lb.mjs} +3 -3
  81. package/dist/{run-env-CQRnxNht.mjs.map → run-env-CkAmx3lb.mjs.map} +1 -1
  82. package/dist/{pai-home-UncxWxlX.mjs → runtime-paths-BiEi_gaG.mjs} +50 -2
  83. package/dist/runtime-paths-BiEi_gaG.mjs.map +1 -0
  84. package/dist/{sources-CM2g-CLT.mjs → sources-BxXbzpAJ.mjs} +1 -1
  85. package/dist/{sources-CM2g-CLT.mjs.map → sources-BxXbzpAJ.mjs.map} +1 -1
  86. package/dist/{zettelkasten-G0-ZTybr.mjs → zettelkasten-CzcPzEsL.mjs} +5 -3
  87. package/dist/{zettelkasten-BRIaHzFm.mjs → zettelkasten-wh2Yy1nS.mjs} +5 -4
  88. package/dist/zettelkasten-wh2Yy1nS.mjs.map +1 -0
  89. package/docker/docker-compose.yml +4 -0
  90. package/docs/commands/README.md +6 -0
  91. package/docs/commands/memory.md +52 -0
  92. package/docs/commands/worker.md +22 -0
  93. package/docs/install-linux.md +1 -0
  94. package/docs/memory.md +69 -0
  95. package/package.json +1 -1
  96. package/src/hooks/ts/lib/sleep-poll-gate.test.ts +2 -2
  97. package/src/hooks/ts/lib/sleep-poll-gate.ts +4 -4
  98. package/src/hooks/ts/lib/worker-guard-longwait.test.ts +48 -0
  99. package/src/hooks/ts/lib/worker-guard.ts +50 -6
  100. package/src/hooks/ts/pre-tool-use/block-sleep-poll.test.ts +1 -1
  101. package/src/hooks/ts/session-start/load-core-context.ts +11 -8
  102. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +34 -2
  103. package/dist/config-Ddc4DIPk.mjs.map +0 -1
  104. package/dist/daemon-BHQoEjt0.mjs +0 -18
  105. package/dist/daemon-vLtT3yaI.mjs.map +0 -1
  106. package/dist/embeddings-D0WOQpYR.mjs +0 -3
  107. package/dist/factory-CXFHwvcv.mjs.map +0 -1
  108. package/dist/factory-CnKsU6zX.mjs +0 -8
  109. package/dist/main-resolver-DrHtx8iW.mjs +0 -7
  110. package/dist/pai-home-UncxWxlX.mjs.map +0 -1
  111. package/dist/postgres-B2BEBT-J.mjs +0 -5
  112. package/dist/postgres-BxmyQHoA.mjs.map +0 -1
  113. package/dist/program-CfG1T_ko.mjs.map +0 -1
  114. package/dist/router-BK-hFeQ6.mjs +0 -3
  115. package/dist/run-ChlIbDJ0.mjs.map +0 -1
  116. package/dist/runtime-paths-Bw3Jk8qJ.mjs +0 -52
  117. package/dist/runtime-paths-Bw3Jk8qJ.mjs.map +0 -1
  118. package/dist/zettelkasten-BRIaHzFm.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-CkbX-hV7.mjs","names":["contentToText","contentToText","cosineSimilarity","folderFromPath","dominantType","folderFromPath","getQueueStats","createServer"],"sources":["../src/notifications/config.ts","../src/workers/keepalive.ts","../src/workers/supervision.ts","../src/memory/vault/walk.ts","../src/memory/vault/deduplicate.ts","../src/memory/vault/parse-links.ts","../src/memory/vault/name-index.ts","../src/memory/vault/resolve.ts","../src/memory/vault/indexer.ts","../src/daemon/templates/context-handover-prompt.ts","../src/daemon/context-handover-worker.ts","../src/daemon/topic-detect-worker.ts","../src/daemon/work-queue-worker.ts","../src/daemon/daemon/scheduler.ts","../src/mcp/tools/types.ts","../src/memory/link-boost.ts","../src/mcp/tools/memory.ts","../src/mcp/tools/projects.ts","../src/mcp/tools/sessions.ts","../src/mcp/tools/registry.ts","../src/mcp/tools/zettel.ts","../src/mcp/tools/kg.ts","../src/mcp/tools/wakeup.ts","../src/memory/taxonomy.ts","../src/mcp/tools/taxonomy.ts","../src/memory/tunnels.ts","../src/mcp/tools/tunnels.ts","../src/mcp/tools/feedback.ts","../src/memory/kg-search.ts","../src/mcp/tools/kg-search.ts","../src/graph/clusters.ts","../src/graph/neighborhood.ts","../src/graph/note-context.ts","../src/graph/trace.ts","../src/graph/latent-ideas.ts","../src/daemon/daemon/dispatcher.ts","../src/daemon/daemon/handler.ts","../src/daemon/dashboard-page.ts","../src/daemon/dashboard.ts","../src/daemon/daemon/dashboard-live.ts","../src/storage/postgres/fts-migration.ts","../src/daemon/daemon/server.ts"],"sourcesContent":["/**\n * config.ts — Notification config persistence helpers\n *\n * Reads and writes the `notifications` section of CONFIG_FILE (see\n * src/daemon/config.ts's paiConfigFilePath).\n * Deep-merges with defaults so partial configs work fine.\n *\n * This module is intentionally separate from the daemon's config loader\n * so it can be used standalone (e.g. from CLI commands).\n */\n\nimport { expandHome, readMainConfigRaw, writeMainConfigRaw } from \"../daemon/config.js\";\nimport type {\n NotificationConfig,\n ChannelConfigs,\n RoutingTable,\n NotificationMode,\n} from \"./types.js\";\nimport {\n DEFAULT_NOTIFICATION_CONFIG,\n DEFAULT_CHANNELS,\n DEFAULT_ROUTING,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Deep merge helper (same approach as daemon/config.ts)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Load\n// ---------------------------------------------------------------------------\n\n/**\n * Load the notification config from the PAI config file.\n * Returns defaults merged with any stored values.\n */\nexport function loadNotificationConfig(): NotificationConfig {\n let parsed: Record<string, unknown>;\n try {\n parsed = readMainConfigRaw();\n } catch {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n const stored = parsed[\"notifications\"];\n if (!stored || typeof stored !== \"object\") {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n return deepMerge(\n DEFAULT_NOTIFICATION_CONFIG,\n stored as Record<string, unknown>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Save\n// ---------------------------------------------------------------------------\n\n/**\n * Persist the notification config by merging it into the existing\n * CONFIG_FILE. Creates the file if it does not exist.\n */\nexport function saveNotificationConfig(config: NotificationConfig): void {\n let full: Record<string, unknown> = {};\n try {\n full = readMainConfigRaw();\n } catch {\n // Start fresh if the file is unreadable\n }\n\n // Replace the notifications section\n full[\"notifications\"] = config;\n\n writeMainConfigRaw(full);\n}\n\n// ---------------------------------------------------------------------------\n// Patch helpers (used by the set command)\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a partial update to the current notification config and persist it.\n * Returns the new merged config.\n */\nexport function patchNotificationConfig(patch: {\n mode?: NotificationMode;\n channels?: Partial<Partial<ChannelConfigs>>;\n routing?: Partial<RoutingTable>;\n}): NotificationConfig {\n const current = loadNotificationConfig();\n\n if (patch.mode !== undefined) {\n current.mode = patch.mode;\n }\n\n if (patch.channels) {\n current.channels = deepMerge(\n current.channels,\n patch.channels as Record<string, unknown>\n );\n }\n\n if (patch.routing) {\n current.routing = deepMerge(\n current.routing,\n patch.routing as Record<string, unknown>\n );\n }\n\n saveNotificationConfig(current);\n return current;\n}\n\n// Re-export defaults for convenience\nexport { DEFAULT_NOTIFICATION_CONFIG, DEFAULT_CHANNELS, DEFAULT_ROUTING };\nexport { expandHome };\n","/**\n * keepalive.ts — one cache-keepalive beat, shared by the daemon scheduler and\n * the probe script.\n *\n * The provider (GLM anthropic-compat endpoint) caches the worker prefix\n * implicitly — no cache_control blocks — so a trivial single-turn worker spawn\n * through the very same runWorker path re-arms that cache for the next real\n * worker: identical env, identical tool grants, identical WORKER_CONTRACT_PROMPT\n * by construction, because it IS a worker spawn.\n *\n * Measured 2026-09-18 (docs/cache-keepalive.md): the implicit cache only\n * serves spawns within seconds of the previous identical request and never\n * survived a 60 s beat cadence in the armed proof — hence the knob defaults\n * to off and arming it is the operator's explicit call.\n *\n * Deliberately NOT a hand-built claude invocation: duplicated prefix\n * construction is how this repo has shipped real breakage before (probeResume\n * x3, archiver x2). Everything flows through runWorker.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { runWorker, type StreamEvent } from \"./run.js\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { eventsPath, ledgerPath, workersLogDir } from \"./paths.js\";\nimport { loadStatus } from \"./status.js\";\n\n/**\n * The trivial prompt every cache beat (and probe) carries. Kept in one place\n * so the heartbeat arms exactly the prefix a verifying probe measures.\n */\nexport const HEARTBEAT_PROMPT = \"Reply with exactly one word: pong.\";\n\n/** The worker class a beat runs as — the cheapest spawn the config defines. */\nexport const KEEPALIVE_CLASS = \"simple\";\n\n/** Worker label of every beat, so `pai worker ps` can tell them apart. */\nexport const KEEPALIVE_LABEL = \"cache-keepalive\";\n\n/** Cadence a keepalive runs at for this config; 0 keeps it off. */\nexport function keepaliveSecs(workers: { enabled: boolean; cacheKeepaliveSecs: number }): number {\n return workers.enabled ? workers.cacheKeepaliveSecs : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Transcript parsing — the only real usage on this endpoint is the final\n// result event (per-turn assistant usage is zeroed), so a beat and a probe\n// both read exactly that line.\n// ---------------------------------------------------------------------------\n\n/**\n * The LAST `type:\"result\"` event of a worker transcript, or null when the\n * file is missing, unreadable, or carries no result line (a failed run can\n * end without one).\n */\nexport function parseLastResultEvent(path: string): StreamEvent | null {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n let found: StreamEvent | null = null;\n for (const line of text.split(\"\\n\")) {\n if (!line.startsWith(\"{\")) continue;\n let e: StreamEvent;\n try {\n e = JSON.parse(line) as StreamEvent;\n } catch {\n continue;\n }\n if (e.type === \"result\") found = e;\n }\n return found;\n}\n\n/** One worker's cache metrics, as reported by its final result event. */\nexport interface BeatMetrics {\n id: string;\n ok: boolean;\n rc: number | null;\n provider: string | null;\n model: string | null;\n input_tokens: number | null;\n output_tokens: number | null;\n cache_read_input_tokens: number | null;\n cache_creation_input_tokens: number | null;\n duration_api_ms: number | null;\n duration_ms: number | null;\n num_turns: number | null;\n}\n\n/**\n * Metrics of one run from its final result event. Single-turn runs make the\n * cumulative result usage identical to first-turn usage, so a warm cache\n * shows up directly as cache_read_input_tokens > 0.\n */\nexport function extractBeatMetrics(\n m: Omit<BeatMetrics, \"input_tokens\" | \"output_tokens\" | \"cache_read_input_tokens\" | \"cache_creation_input_tokens\" | \"duration_api_ms\" | \"duration_ms\" | \"num_turns\">,\n ev: StreamEvent | null\n): BeatMetrics {\n const u = ev?.usage;\n return {\n ...m,\n input_tokens: u?.input_tokens ?? null,\n output_tokens: u?.output_tokens ?? null,\n cache_read_input_tokens: u?.cache_read_input_tokens ?? null,\n cache_creation_input_tokens: u?.cache_creation_input_tokens ?? null,\n duration_api_ms: ev?.duration_api_ms ?? null,\n duration_ms: ev?.duration_ms ?? null,\n num_turns: ev?.num_turns ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The beat\n// ---------------------------------------------------------------------------\n\nexport interface BeatOptions {\n /** Config path override (tests never touch the live config). */\n configPath?: string;\n /** Preset worker id (tests); a fresh stamp when omitted. */\n id?: string;\n}\n\n/** True while a beat's worker is still running — the overlap guard reads it. */\nlet beatInFlight = false;\n\nexport function beatBusy(): boolean {\n return beatInFlight;\n}\n\n/** `keepalive-<YYYYMMDD-HHMMSS>` — beats are minutes apart, seconds suffice. */\nexport function beatId(d: Date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `keepalive-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` +\n `-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`\n );\n}\n\n/**\n * One heartbeat: a single-turn trivial worker through the exact worker spawn\n * path, its result metrics parsed from the transcript, one WORKER-KEEPALIVE\n * ledger line. Returns null when another beat is still running (overlap\n * guard) — a skipped beat is free, a queue of them is not.\n *\n * The beat runs in the workers logDir: no session renders its status line\n * there, so the spawn adopts no spawnerSession and no iTerm identity, and\n * worker supervision has no owner to notify — heartbeats are infrastructure,\n * not somebody's workers.\n */\nexport async function runKeepaliveBeat(opts: BeatOptions = {}): Promise<BeatMetrics | null> {\n if (beatInFlight) return null;\n beatInFlight = true;\n try {\n const { workers: config } = readWorkersSection(opts.configPath);\n const logDir = workersLogDir(config);\n const id = opts.id ?? beatId();\n const rc = await runWorker({\n claudeArgs: [\"-p\", HEARTBEAT_PROMPT],\n className: KEEPALIVE_CLASS,\n label: KEEPALIVE_LABEL,\n noPane: true,\n quiet: true,\n worktreeFlag: false,\n id,\n cwd: logDir,\n });\n const status = loadStatus(logDir, id);\n const metrics = extractBeatMetrics(\n {\n id,\n ok: rc === 0,\n rc,\n provider: status?.provider ?? null,\n model: status?.model ?? null,\n },\n parseLastResultEvent(eventsPath(logDir, id))\n );\n appendLedger(ledgerPath(logDir), \"WORKER-KEEPALIVE\", {\n id,\n provider: metrics.provider,\n model: metrics.model,\n input_tokens: metrics.input_tokens,\n cache_read_input_tokens: metrics.cache_read_input_tokens,\n cache_creation_input_tokens: metrics.cache_creation_input_tokens,\n duration_api_ms: metrics.duration_api_ms,\n });\n return metrics;\n } finally {\n beatInFlight = false;\n }\n}\n","/**\n * supervision.ts — daemon-side worker supervision, event push not polling.\n *\n * An orchestrating session must keep an eye on its workers without sleeping\n * (the Worker skill forbids busy-waiting), the same way the AIBroker daemon\n * keeps an eye on sessions. The PAI daemon already runs all day; this module\n * gives it a tick that watches every worker's status file and pushes one-line\n * events to the owning session when something happens:\n *\n * finished — the run ended clean (state done, rc 0)\n * failed — the run ended badly (state failed/killed/lost, or rc != 0,\n * or the runner pid vanished while state was still running)\n * stalled — state running, runner alive, but no new turn for longer than\n * the stall threshold (PAI_WORKER_STALL_MINUTES, default 10)\n * thrashing — state running, runner alive, turning, but the last N tool\n * executions all failed (PAI_WORKER_THRASH_FAILS, default 5) —\n * a worker that keeps failing every command never goes quiet,\n * so the stall detector alone cannot see it\n * sleeping — state running, one Bash command sleeping N >= 60s with nothing\n * to wait on — under the stall threshold for runs like\n * `sleep 500` (8.3m), which would otherwise be invisible until\n * they cross it. The bounded-wait pattern (background a build or\n * test, then `sleep 60; ps` cycles) has live children and stays\n * quiet; stalled and thrashing remain the backstop for a child\n * that never finishes or dies silently\n *\n * The interactive chat pane (origin \"chat\") is never supervised: it is the\n * operator's own session, not a task worker. Its status file flips to done\n * when the session ends, and reporting that as \"worker finished\" to whoever\n * next opens the same tab is noise, not an outcome.\n *\n * Delivery, in order of preference, never both counted as required:\n * 1. AIBroker `send_to_session` into the orchestrator's terminal — the\n * one-line Agentish-style notification, arriving as a user turn without\n * the orchestrator asking for it. Needs the iTerm identity of the owner,\n * which the status line bridges into the session map (see scope.ts).\n * 2. An append-only event file, <logDir>/supervision/<owner>.events, one\n * JSON line per event. The UserPromptSubmit hook\n * (src/hooks/ts/user-prompt/worker-supervision.ts) surfaces undelivered\n * lines into the session that owns them, so the events reach the model\n * on its next turn even with AIBroker absent.\n *\n * Exactly-once: every event carries a deterministic id and the set of\n * delivered ids per worker is persisted (<logDir>/supervision/state.json), so\n * a daemon restart replays nothing. A first run with no state adopts every\n * already-terminal worker as delivered — supervision reports transitions it\n * observes, not history. Chain stages are separate workers with their own\n * status files and are supervised individually.\n *\n * No model is ever called from here — this is daemon code, and it must stay\n * that way: supervision that bills tokens to watch tokens is a bug.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { alive, isChatPane, loadStatuses, nowStamp, type WorkerStatus } from \"./status.js\";\nimport { itermForClaudeSession } from \"./scope.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" in a long-running daemon (prune-dist.mjs deletes superseded chunks).\nimport { sendToSession } from \"../cli/lib/aibroker-client.js\";\n\n// ---------------------------------------------------------------------------\n// Events\n// ---------------------------------------------------------------------------\n\nexport type SupervisionKind = \"finished\" | \"failed\" | \"stalled\" | \"thrashing\" | \"sleeping\";\n\n/** The kinds that fire mid-run and re-arm on turn progress (see eventId). */\nconst MID_RUN_KINDS: readonly SupervisionKind[] = [\"stalled\", \"thrashing\", \"sleeping\"];\n\nexport interface SupervisionEvent {\n /** Deterministic dedup key — see eventId(). */\n id: string;\n /** nowStamp() at detection. */\n ts: string;\n kind: SupervisionKind;\n worker: string;\n label: string;\n /** Owning session key: the spawner's claude session id, or the iTerm uuid. */\n session: string;\n /** Exit code when the run reported one, else null. */\n rc: number | null;\n /** Minutes without a turn, for stalled events; else null. */\n stalledMin: number | null;\n /** Consecutive failed commands, for thrashing events; else null. */\n consecFails?: number | null;\n /** Seconds of the one sleeping command, for sleeping events; else null. */\n sleepSec?: number | null;\n /** The one-liner that reaches the orchestrator. */\n text: string;\n}\n\n/**\n * The owning session of a worker, or null when nobody claims it (and so\n * nothing to notify): the spawner's claude session id first — that is what\n * the hook knows a session by — else the AIBroker iTerm uuid.\n */\nexport function ownershipKey(s: WorkerStatus): string | null {\n if (s.spawnerSession) return s.spawnerSession;\n if (s.session?.id) return s.session.id;\n return null;\n}\n\nconst TERMINAL: readonly WorkerStatus[\"state\"][] = [\"done\", \"failed\", \"killed\", \"lost\"];\n\nfunction isTerminal(s: WorkerStatus): boolean {\n return TERMINAL.includes(s.state);\n}\n\n/** Age of a \"YYYY-MM-DD HH:MM:SS\" stamp in ms, NaN-safe (unparsable → 0). */\nfunction ageMs(ts: string, now: Date): number {\n const t = Date.parse(ts.replace(\" \", \"T\"));\n if (Number.isNaN(t)) return 0;\n return Math.max(0, now.getTime() - t);\n}\n\n/**\n * The stall threshold in minutes from PAI_WORKER_STALL_MINUTES (default 10).\n * Anything unparsible or non-positive falls back to the default — a broken\n * value must not turn supervision off or make it fire every tick.\n */\nexport const DEFAULT_STALL_MINUTES = 10;\n\nexport function stallMinutesFromEnv(env: NodeJS.ProcessEnv = process.env): number {\n const raw = env.PAI_WORKER_STALL_MINUTES;\n if (raw === undefined || raw === \"\") return DEFAULT_STALL_MINUTES;\n const n = Number(raw);\n return Number.isFinite(n) && n > 0 ? n : DEFAULT_STALL_MINUTES;\n}\n\n/**\n * The consecutive-failure threshold from PAI_WORKER_THRASH_FAILS (default 5),\n * with the same broken-value tolerance as stallMinutesFromEnv.\n */\nexport const DEFAULT_THRASH_FAILS = 5;\n\nexport function thrashFailsFromEnv(env: NodeJS.ProcessEnv = process.env): number {\n const raw = env.PAI_WORKER_THRASH_FAILS;\n if (raw === undefined || raw === \"\") return DEFAULT_THRASH_FAILS;\n const n = Number(raw);\n return Number.isFinite(n) && n > 0 ? n : DEFAULT_THRASH_FAILS;\n}\n\n/**\n * Event id: `<worker>#<kind>` for the once-per-run terminal kinds, and\n * `<worker>#<kind>@<turns>` for the mid-run kinds (stalled, thrashing,\n * sleeping) — the turn count re-arms the id, so a worker that resumes and\n * hits the same condition again gets a second event while a restart cannot\n * replay the first.\n */\nexport function eventId(worker: string, kind: SupervisionKind, turns = 0): string {\n return MID_RUN_KINDS.includes(kind) ? `${worker}#${kind}@${turns}` : `${worker}#${kind}`;\n}\n\n/**\n * Live child pids of a runner via `pgrep -P` (its exit 1 means \"no\n * children\"). Any failure — here or in an injected stub — is caught at the\n * one call site in detectSupervisionEvents and reads as an empty list, so a\n * broken check degrades to firing the sleeping event, never to silencing it.\n */\nfunction pgrepChildren(pid: number): number[] {\n return execFileSync(\"pgrep\", [\"-P\", String(pid)], { encoding: \"utf8\" })\n .split(\"\\n\")\n .flatMap((l) => {\n const n = Number(l.trim());\n return Number.isInteger(n) && n > 0 ? [n] : [];\n });\n}\n\nexport interface DetectOptions {\n /** How long a running worker may go without a turn before \"stalled\". */\n stallMs: number;\n now: Date;\n /** Consecutive failed tool executions before \"thrashing\" (default 5). */\n thrashFails?: number;\n /** pid liveness — injected so tests need no real processes. */\n isAlive?: (pid: number) => boolean;\n /**\n * Live child pids of a runner — a sleeping worker with children is a\n * bounded wait on real work, not a stuck one. Injected so tests need no\n * pgrep; the default is pgrepChildren.\n */\n childPids?: (pid: number) => number[];\n /**\n * How stale `updated` must be before a dead pid counts as killed rather\n * than as a status write racing the tick.\n */\n deadGraceMs?: number;\n}\n\n/** One event's one-liner — the exact line the orchestrator reads. */\nfunction eventText(kind: SupervisionKind, s: WorkerStatus, rc: number | null, stalledMin: number | null): string {\n const tail = `- see pai worker replay ${s.id}`;\n if (kind === \"finished\") return `worker ${s.id} ${s.label} finished rc=0 ${tail}`;\n if (kind === \"stalled\") return `worker ${s.id} ${s.label} stalled ${stalledMin}m no turns ${tail}`;\n if (kind === \"thrashing\")\n return `worker ${s.id} ${s.label} thrashing: ${s.consecFails} consecutive failed commands ${tail}`;\n if (kind === \"sleeping\") return `worker ${s.id} ${s.label} sleeping ${s.sleepSec}s in one command ${tail}`;\n const why = rc === null ? \"runner gone\" : `rc=${rc}`;\n return `worker ${s.id} ${s.label} failed ${why} ${tail}`;\n}\n\n/**\n * Detect conditions across a ledger snapshot. Pure: statuses in, events out;\n * dedup against delivered ids is the caller's job (see filterUndelivered).\n * Workers without an owning session are skipped — nobody to notify.\n */\nexport function detectSupervisionEvents(\n statuses: WorkerStatus[],\n opts: DetectOptions\n): SupervisionEvent[] {\n const isAlive = opts.isAlive ?? alive;\n const deadGraceMs = opts.deadGraceMs ?? 15_000;\n const out: SupervisionEvent[] = [];\n for (const s of statuses) {\n const session = ownershipKey(s);\n if (!session) continue;\n // the operator's own pane: ending it is not a worker outcome, idling is\n // not a stall — nothing here is worth a push into anybody's terminal\n if (isChatPane(s)) continue;\n let kind: SupervisionKind | null = null;\n let rc: number | null = null;\n let stalledMin: number | null = null;\n if (isTerminal(s)) {\n // a finished run is state done with rc 0; every other terminal shape\n // (failed, killed, lost, done with a non-zero rc) is a failure\n if (s.state === \"done\" && (s.rc ?? 0) === 0) {\n kind = \"finished\";\n rc = 0;\n } else {\n kind = \"failed\";\n rc = s.state === \"lost\" ? null : s.rc;\n }\n } else if (!isAlive(s.pid) && ageMs(s.updated, opts.now) > deadGraceMs) {\n // state says running but the runner is gone (SIGKILL, crash) — the\n // status will never get its rc, so supervision is the only witness\n kind = \"failed\";\n rc = null;\n } else if (ageMs(s.updated, opts.now) > opts.stallMs) {\n kind = \"stalled\";\n stalledMin = Math.floor(ageMs(s.updated, opts.now) / 60_000);\n } else if ((s.consecFails ?? 0) >= (opts.thrashFails ?? DEFAULT_THRASH_FAILS)) {\n kind = \"thrashing\";\n } else if (s.sleepSec != null) {\n // bounded wait on real work: a sleep while a spawned child still runs\n // is healthy, so flag only a sleep with nothing to wait on. The check\n // runs once per sleeping candidate, only for a live pid; a failed\n // check reads as no children and fires. stalled (no turns) and\n // thrashing stay the backstop and never pass through here.\n let hasChildren = false;\n if (isAlive(s.pid)) {\n try {\n hasChildren = (opts.childPids ?? pgrepChildren)(s.pid).length > 0;\n } catch {\n hasChildren = false; // pgrep gone: degrade to today's behavior\n }\n }\n if (!hasChildren) kind = \"sleeping\";\n }\n if (!kind) continue;\n out.push({\n id: eventId(s.id, kind, s.turns),\n ts: nowStamp(opts.now),\n kind,\n worker: s.id,\n label: s.label,\n session,\n rc,\n stalledMin,\n ...(kind === \"thrashing\" ? { consecFails: s.consecFails ?? 0 } : {}),\n ...(kind === \"sleeping\" ? { sleepSec: s.sleepSec ?? 0 } : {}),\n text: eventText(kind, s, rc, stalledMin),\n });\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Delivered-id persistence — the restart guard\n// ---------------------------------------------------------------------------\n\nexport interface SupervisionState {\n /** worker id → event ids already written to that owner's event file. */\n delivered: Record<string, string[]>;\n}\n\n/** Cap on remembered workers: terminal and delivered is history, not state. */\nconst MAX_REMEMBERED_WORKERS = 500;\n\nexport function supervisionDirPath(logDir: string): string {\n return join(logDir, \"supervision\");\n}\n\nexport function supervisionStatePath(logDir: string): string {\n return join(supervisionDirPath(logDir), \"state.json\");\n}\n\nexport function supervisionEventsPath(logDir: string, session: string): string {\n return join(supervisionDirPath(logDir), `${session}.events`);\n}\n\n/** Receipts of events already pushed into a terminal, one event id per line. */\nexport function supervisionPushedPath(logDir: string, session: string): string {\n return join(supervisionDirPath(logDir), `${session}.pushed`);\n}\n\nexport function loadSupervisionState(path: string): { state: SupervisionState; fresh: boolean } {\n if (!existsSync(path)) return { state: { delivered: {} }, fresh: true };\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as SupervisionState;\n if (parsed && typeof parsed === \"object\" && typeof parsed.delivered === \"object\") {\n return { state: parsed, fresh: false };\n }\n } catch {\n // a damaged state file replays at most one round of terminal events;\n // rewriting it is the recovery, not the problem\n }\n return { state: { delivered: {} }, fresh: true };\n}\n\nexport function saveSupervisionState(path: string, state: SupervisionState): void {\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n const tmp = `${path}.tmp`;\n writeFileSync(tmp, JSON.stringify(state), \"utf8\");\n renameSync(tmp, path);\n}\n\n/**\n * Drop the delivered ids of workers the ledger no longer lists (absent means\n * history: their status files are gone) and cap only that history — newest\n * ids last, worker ids sort chronologically — so the persisted state stays\n * bounded. A worker the ledger still lists must never lose its receipts:\n * pruning one un-marks its terminal events as delivered, and every save\n * re-arms them into an unbounded re-delivery loop (the 2026-09-25 incident:\n * one old chain stage re-pushed 7,393 times, because the cap of 500 bit into\n * a ledger of ~1,700 live statuses).\n */\nexport function pruneState(\n state: SupervisionState,\n statuses: WorkerStatus[],\n maxAbsent = MAX_REMEMBERED_WORKERS\n): SupervisionState {\n const present = new Set(statuses.map((s) => s.id));\n const entries = Object.entries(state.delivered);\n const byId = ([a]: [string, string[]], [b]: [string, string[]]) => (a < b ? -1 : a > b ? 1 : 0);\n const keptPresent = entries.filter(([id]) => present.has(id)).sort(byId);\n const absent = entries.filter(([id]) => !present.has(id)).sort(byId);\n // not slice(-maxAbsent): with maxAbsent 0 that returns the whole array\n const keptAbsent = absent.slice(Math.max(0, absent.length - maxAbsent));\n return { delivered: Object.fromEntries([...keptAbsent, ...keptPresent]) };\n}\n\n/**\n * First run: adopt every already-terminal worker as delivered. Supervision\n * reports the transitions it observes from now on, never a replay of every\n * worker that ever finished before it started.\n */\nexport function baselineState(statuses: WorkerStatus[]): SupervisionState {\n const delivered: Record<string, string[]> = {};\n for (const s of statuses) {\n if (!isTerminal(s) || !ownershipKey(s)) continue;\n delivered[s.id] = [eventId(s.id, \"finished\"), eventId(s.id, \"failed\")];\n }\n return { delivered };\n}\n\n/** The events whose ids the state has not seen yet. */\nexport function filterUndelivered(events: SupervisionEvent[], state: SupervisionState): SupervisionEvent[] {\n return events.filter((e) => !(state.delivered[e.worker] ?? []).includes(e.id));\n}\n\nexport function markDelivered(state: SupervisionState, events: SupervisionEvent[]): void {\n for (const e of events) {\n const ids = state.delivered[e.worker] ?? [];\n if (!ids.includes(e.id)) ids.push(e.id);\n state.delivered[e.worker] = ids;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Delivery\n// ---------------------------------------------------------------------------\n\n/** Where an event's push goes, resolved from the worker's own identities. */\nexport interface PushTarget {\n claudeSession: string | null;\n iterm: string | null;\n}\n\nexport type PushFn = (logDir: string, target: PushTarget, ev: SupervisionEvent) => Promise<boolean>;\n\n/**\n * Default push: AIBroker `send_to_session` into the orchestrator's terminal,\n * using the iTerm uuid the worker recorded (`session.id`) or the one the\n * session map bridges for the spawner's claude session. A timeout still\n * delivered (see aibroker-client) counts as delivered; every failure is\n * reported as not-delivered so only the event file carries the event.\n */\nexport const aibrokerPush: PushFn = async (logDir, target, ev) => {\n const iterm =\n target.iterm ?? (target.claudeSession ? itermForClaudeSession(logDir, target.claudeSession) : null);\n if (!iterm) return false;\n try {\n const r = await sendToSession(iterm, ev.text);\n return r.ok || r.timedOut === true;\n } catch {\n return false;\n }\n};\n\n/**\n * Whether an event should be relayed into the owner's terminal. The mid-run\n * kinds (stalled, thrashing, sleeping) ALWAYS relay: a json/stream-json\n * caller knows the run's result at exit but has no other way to hear\n * mid-run trouble. Only the terminal kinds (finished/failed) skip the relay\n * for those callers — they read the run's result itself, and the harness\n * already notifies them on process exit, so the one-liner would cost a full\n * extra turn for information they already have. Interactive/pane launches\n * and the default text format are never skipped.\n */\nexport function shouldRelay(\n kind: SupervisionKind,\n outputFormat: WorkerStatus[\"outputFormat\"] | undefined\n): boolean {\n if (MID_RUN_KINDS.includes(kind)) return true;\n return outputFormat !== \"json\" && outputFormat !== \"stream-json\";\n}\n\nfunction appendEventFile(logDir: string, ev: SupervisionEvent): void {\n const path = supervisionEventsPath(logDir, ev.session);\n const dir = supervisionDirPath(logDir);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n appendFileSync(path, JSON.stringify(ev) + \"\\n\", \"utf8\");\n}\n\n/** Record that an event reached its owner's terminal — the hook then skips it. */\nfunction appendPushReceipt(logDir: string, ev: SupervisionEvent): void {\n try {\n appendFileSync(supervisionPushedPath(logDir, ev.session), `${ev.id}\\n`, \"utf8\");\n } catch {\n // a missing receipt only means the hook may repeat the line\n }\n}\n\nexport interface TickOptions {\n now?: Date;\n stallMs?: number;\n /** Consecutive-failure threshold for thrashing (default: PAI_WORKER_THRASH_FAILS). */\n thrashFails?: number;\n isAlive?: (pid: number) => boolean;\n /** Injectable for tests; production pushes through AIBroker. */\n push?: PushFn;\n /** Preloaded map, so a test (or a batch of ticks) can share one read. */\n statuses?: WorkerStatus[];\n}\n\nexport interface TickResult {\n events: SupervisionEvent[];\n /** Events that also reached a terminal through the push channel. */\n pushed: SupervisionEvent[];\n}\n\n/**\n * One supervision pass over the ledger: detect, append each new event to its\n * owner's event file, push it best effort, persist the delivered ids. The\n * event file is the durable record — an event is marked delivered only after\n * its line landed there, so a failed tick retries, a repeated one replays\n * nothing.\n */\nexport async function runSupervisionTick(logDir: string, opts: TickOptions = {}): Promise<TickResult> {\n const now = opts.now ?? new Date();\n const statuses = opts.statuses ?? loadStatuses(logDir);\n const statePath = supervisionStatePath(logDir);\n const loaded = loadSupervisionState(statePath);\n let state = loaded.state;\n if (loaded.fresh) state = baselineState(statuses);\n\n const pending = filterUndelivered(\n detectSupervisionEvents(statuses, {\n stallMs: opts.stallMs ?? DEFAULT_STALL_MINUTES * 60_000,\n thrashFails: opts.thrashFails ?? thrashFailsFromEnv(),\n now,\n ...(opts.isAlive ? { isAlive: opts.isAlive } : {}),\n }),\n state\n );\n\n const written: SupervisionEvent[] = [];\n for (const ev of pending) {\n appendEventFile(logDir, ev);\n written.push(ev);\n }\n\n const push = opts.push ?? aibrokerPush;\n const pushed: SupervisionEvent[] = [];\n for (const ev of written) {\n // the owner's iTerm identity is in its worker status, not the event\n const owner = statuses.find((s) => s.id === ev.worker);\n if (!shouldRelay(ev.kind, owner?.outputFormat)) continue; // caller reads its own JSON result\n const target: PushTarget = {\n claudeSession: owner?.spawnerSession ?? null,\n iterm: owner?.session?.id ?? null,\n };\n let ok = false;\n try {\n ok = await push(logDir, target, ev);\n } catch {\n ok = false;\n }\n if (ok) {\n pushed.push(ev);\n appendPushReceipt(logDir, ev);\n }\n }\n\n markDelivered(state, written);\n saveSupervisionState(statePath, pruneState(state, statuses));\n return { events: written, pushed };\n}\n","/**\n * Vault directory walker — follows symlinks with cycle detection.\n */\n\nimport { statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/** Maximum number of .md files to collect from a vault. */\nconst VAULT_MAX_FILES = 10_000;\n\n/** Maximum recursion depth for vault directory walks. */\nconst VAULT_MAX_DEPTH = 10;\n\n/**\n * Directories to always skip, at any depth, during vault walks.\n * Includes standard build/VCS noise plus Obsidian-specific directories.\n */\nconst VAULT_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\",\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\",\n \".next\",\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Obsidian internals\n \".obsidian\",\n \".trash\",\n]);\n\n/**\n * Recursively collect all .md files under a vault root, following symlinks.\n *\n * Symlink-following behaviour:\n * - Symbolic links to files: followed if the target is a .md file\n * - Symbolic links to directories: followed with cycle detection via inode\n *\n * Cycle detection is based on the real inode of each visited directory.\n * Using the real stat (not lstat) ensures that symlinked dirs resolve to\n * their actual inode, preventing infinite loops.\n *\n * @param vaultRoot Absolute path to the vault root directory.\n * @param opts Optional overrides for maxFiles and maxDepth.\n */\nexport function walkVaultMdFiles(\n vaultRoot: string,\n opts?: { maxFiles?: number; maxDepth?: number },\n): VaultFile[] {\n const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;\n const maxDepth = opts?.maxDepth ?? VAULT_MAX_DEPTH;\n\n const results: VaultFile[] = [];\n const visitedDirs = new Set<string>();\n\n function walk(dir: string, depth: number): void {\n if (results.length >= maxFiles) return;\n if (depth > maxDepth) return;\n\n // Get the real inode of this directory (follows symlinks on the dir itself)\n let dirStat: ReturnType<typeof statSync>;\n try {\n dirStat = statSync(dir);\n } catch {\n return; // Unreadable or broken symlink — skip\n }\n\n const dirKey = `${dirStat.dev}:${dirStat.ino}`;\n if (visitedDirs.has(dirKey)) return; // Cycle detected\n visitedDirs.add(dirKey);\n\n let entries: import(\"node:fs\").Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: \"utf8\" });\n } catch {\n return; // Unreadable directory — skip\n }\n\n for (const entry of entries) {\n if (results.length >= maxFiles) break;\n if (VAULT_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(dir, entry.name);\n\n if (entry.isSymbolicLink()) {\n // Follow the symlink — resolve to real target\n let targetStat: ReturnType<typeof statSync>;\n try {\n targetStat = statSync(full); // statSync follows symlinks\n } catch {\n continue; // Broken symlink — skip\n }\n\n if (targetStat.isDirectory()) {\n if (!VAULT_SKIP_DIRS.has(entry.name)) {\n walk(full, depth + 1);\n }\n } else if (targetStat.isFile() && entry.name.endsWith(\".md\")) {\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: targetStat.ino,\n device: targetStat.dev,\n });\n }\n } else if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n let fileStat: ReturnType<typeof statSync>;\n try {\n fileStat = statSync(full);\n } catch {\n continue;\n }\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: fileStat.ino,\n device: fileStat.dev,\n });\n }\n }\n }\n\n if (existsSync(vaultRoot)) {\n walk(vaultRoot, 0);\n }\n\n return results;\n}\n","/**\n * Inode-based deduplication for vault files.\n */\n\nimport type { VaultFile, InodeGroup } from \"./types.js\";\n\n/**\n * Group vault files by inode identity (device + inode).\n *\n * Within each group, the canonical file is chosen as the one with the\n * fewest path separators (shallowest), breaking ties by shortest string.\n * All other group members become aliases.\n */\nexport function deduplicateByInode(files: VaultFile[]): InodeGroup[] {\n const groups = new Map<string, VaultFile[]>();\n\n for (const file of files) {\n const key = `${file.device}:${file.inode}`;\n const existing = groups.get(key);\n if (existing) {\n existing.push(file);\n } else {\n groups.set(key, [file]);\n }\n }\n\n const result: InodeGroup[] = [];\n\n for (const group of groups.values()) {\n if (group.length === 0) continue;\n\n // Sort: fewest path separators first, then shortest string\n const sorted = [...group].sort((a, b) => {\n const aDepth = (a.vaultRelPath.match(/\\//g) ?? []).length;\n const bDepth = (b.vaultRelPath.match(/\\//g) ?? []).length;\n if (aDepth !== bDepth) return aDepth - bDepth;\n return a.vaultRelPath.length - b.vaultRelPath.length;\n });\n\n const [canonical, ...aliases] = sorted as [VaultFile, ...VaultFile[]];\n result.push({ canonical, aliases });\n }\n\n return result;\n}\n","/**\n * Markdown link parser for vault files.\n *\n * Handles wikilinks ([[target]]), markdown links ([text](path)),\n * embeds (![[target]]), and frontmatter wikilinks.\n */\n\nimport type { ParsedLink } from \"./types.js\";\n\n/**\n * Parse all links from markdown content.\n *\n * Handles:\n * - Standard wikilinks: [[Target Note]]\n * - Aliased wikilinks: [[Target Note|Display Text]]\n * - Heading anchors: [[Target Note#Heading]] (stripped for resolution)\n * - Embeds: ![[Target Note]]\n * - Frontmatter wikilinks (YAML between --- delimiters)\n * - Markdown links: [text](path/to/note.md)\n * - Markdown embeds: ![alt](image.png)\n *\n * External URLs (http://, https://, mailto:, etc.) are excluded — only\n * relative paths are treated as vault links.\n *\n * @param content Raw markdown file content.\n * @returns Array of parsed links in document order.\n */\nexport function parseLinks(content: string): ParsedLink[] {\n const links: ParsedLink[] = [];\n const lines = content.split(\"\\n\");\n\n // Determine frontmatter range (YAML between opening and closing ---)\n let frontmatterEnd = 0;\n if (content.startsWith(\"---\")) {\n const closingIdx = content.indexOf(\"\\n---\", 3);\n if (closingIdx !== -1) {\n frontmatterEnd = content.slice(0, closingIdx + 4).split(\"\\n\").length - 1;\n }\n }\n\n // Regex for [[wikilinks]] and ![[embeds]]\n const wikilinkRe = /(!?)\\[\\[([^\\]]+?)\\]\\]/g;\n\n // Regex for markdown links [text](target) and embeds ![alt](target)\n const mdLinkRe = /(!)?\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx]!;\n const lineNumber = lineIdx + 1; // 1-indexed\n const isFrontmatter = lineIdx < frontmatterEnd;\n\n // --- Wikilinks ---\n wikilinkRe.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = wikilinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const inner = match[2]!;\n\n // Split on first | for alias\n const pipeIdx = inner.indexOf(\"|\");\n const beforePipe = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);\n const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1);\n\n // Strip heading anchor (everything after #)\n const hashIdx = beforePipe.indexOf(\"#\");\n const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();\n\n if (!raw) continue; // Skip links with empty targets (e.g. [[#Heading]])\n\n links.push({\n raw,\n alias: alias?.trim() ?? null,\n lineNumber,\n isEmbed: isEmbed && !isFrontmatter,\n isMdLink: false,\n });\n }\n\n // --- Markdown links --- (skip inside frontmatter)\n if (!isFrontmatter) {\n mdLinkRe.lastIndex = 0;\n while ((match = mdLinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const displayText = match[2]!;\n let target = match[3]!.trim();\n\n // Skip external URLs\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;\n\n // Skip pure anchor links (#heading)\n if (target.startsWith(\"#\")) continue;\n\n // Strip heading anchor from target\n const hashIdx = target.indexOf(\"#\");\n if (hashIdx !== -1) target = target.slice(0, hashIdx);\n\n // URL-decode (Obsidian encodes spaces as %20 in md links)\n try {\n target = decodeURIComponent(target);\n } catch {\n // Malformed encoding — use as-is\n }\n\n // Strip .md extension for resolution (resolveWikilink adds it back)\n const raw = target.replace(/\\.md$/i, \"\").trim();\n if (!raw) continue;\n\n links.push({\n raw,\n alias: displayText || null,\n lineNumber,\n isEmbed,\n isMdLink: true,\n });\n }\n }\n }\n\n return links;\n}\n\n/** @deprecated Use {@link parseLinks} instead. */\nexport const parseWikilinks = parseLinks;\n","/**\n * Name index builder for Obsidian wikilink resolution.\n */\n\nimport { basename } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/**\n * Build a name lookup index for Obsidian wikilink resolution.\n *\n * Maps lowercase filename (without .md extension) to all vault-relative paths\n * that share that name. Includes both canonical paths and alias paths so that\n * wikilinks resolve regardless of which path the file is accessed through.\n */\nexport function buildNameIndex(files: VaultFile[]): Map<string, string[]> {\n const index = new Map<string, string[]>();\n\n for (const file of files) {\n const name = basename(file.vaultRelPath, \".md\").toLowerCase();\n const existing = index.get(name);\n if (existing) {\n existing.push(file.vaultRelPath);\n } else {\n index.set(name, [file.vaultRelPath]);\n }\n }\n\n return index;\n}\n","/**\n * Wikilink resolver using Obsidian's shortest-match algorithm.\n */\n\nimport { normalize, basename, dirname } from \"node:path\";\n\n/**\n * Resolve a wikilink target to a vault-relative path using Obsidian's rules.\n *\n * Resolution algorithm:\n * 1. If raw contains \"/\", attempt exact path match (with and without .md).\n * 2. Normalize: lowercase the raw target, strip .md extension.\n * 3. Look up in the name index (all files with that basename).\n * 4. If exactly one match, return it.\n * 5. If multiple matches, pick the one closest to the source file\n * (longest common directory prefix, then shortest overall path).\n * 6. If no matches, return null (dead link).\n *\n * @param raw The raw link target (heading-stripped, pipe-stripped).\n * @param nameIndex Map from lowercase basename-without-ext to vault paths.\n * @param sourcePath Vault-relative path of the file containing the link.\n * @returns Vault-relative path of the resolved target, or null.\n */\nexport function resolveWikilink(\n raw: string,\n nameIndex: Map<string, string[]>,\n sourcePath: string,\n): string | null {\n if (!raw) return null;\n\n // Case 1: path contains \"/\" — try exact match with and without .md\n if (raw.includes(\"/\")) {\n const normalized = normalize(raw);\n const normalizedMd = normalized.endsWith(\".md\") ? normalized : normalized + \".md\";\n\n // Check if any indexed path matches (case-insensitive for macOS compatibility)\n for (const [, paths] of nameIndex) {\n for (const p of paths) {\n if (p === normalizedMd || p === normalized) return p;\n if (p.toLowerCase() === normalizedMd.toLowerCase()) return p;\n }\n }\n // Fall through to name lookup in case the path prefix was wrong\n }\n\n // Normalize the raw target for name lookup.\n // Use the basename only — Obsidian resolves by filename, not full path.\n const rawBase = basename(raw)\n .replace(/\\.md$/i, \"\")\n .toLowerCase()\n .trim();\n\n if (!rawBase) return null;\n\n const candidates = nameIndex.get(rawBase);\n\n if (!candidates || candidates.length === 0) {\n return null; // Dead link\n }\n\n if (candidates.length === 1) {\n return candidates[0]!;\n }\n\n // Multiple matches — pick the one closest to the source file\n const sourceDir = dirname(sourcePath);\n\n let bestPath: string | null = null;\n let bestPrefixLen = -1;\n let bestPathLen = Infinity;\n\n for (const candidate of candidates) {\n const candidateDir = dirname(candidate);\n const prefixLen = commonPrefixLength(sourceDir, candidateDir);\n const pathLen = candidate.length;\n\n if (\n prefixLen > bestPrefixLen ||\n (prefixLen === bestPrefixLen && pathLen < bestPathLen)\n ) {\n bestPrefixLen = prefixLen;\n bestPathLen = pathLen;\n bestPath = candidate;\n }\n }\n\n return bestPath;\n}\n\n/**\n * Compute the length of the common prefix between two directory paths,\n * measured in path segments (not raw characters).\n *\n * Example: \"a/b/c\" and \"a/b/d\" → 2 (common: \"a\", \"b\")\n */\nfunction commonPrefixLength(a: string, b: string): number {\n if (a === \".\" && b === \".\") return 0;\n const aParts = a === \".\" ? [] : a.split(\"/\");\n const bParts = b === \".\" ? [] : b.split(\"/\");\n let count = 0;\n const len = Math.min(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n if (aParts[i] === bParts[i]) {\n count++;\n } else {\n break;\n }\n }\n return count;\n}\n","/**\n * Main vault indexing orchestrator.\n *\n * Indexes an entire Obsidian vault (or any markdown knowledge base), following\n * symlinks, deduplicating files by inode, parsing wikilinks, and computing\n * per-file health metrics (orphan detection, dead links).\n *\n * Key differences from the project indexer (indexer/sync.ts):\n * - Follows symbolic links (project indexer skips them)\n * - Deduplicates files with the same inode (same content reachable via multiple paths)\n * - Parses [[wikilinks]] and builds a directed link graph\n * - Resolves wikilinks using Obsidian's shortest-match algorithm\n * - Computes health metrics per file: inbound/outbound link counts, dead links, orphans\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport type {\n StorageBackend,\n VaultFileRow,\n VaultAliasRow,\n VaultLinkRow,\n VaultHealthRow,\n VaultNameEntry,\n ChunkRow,\n} from \"../../storage/interface.js\";\nimport { chunkMarkdown, fileContentHash } from \"../chunker.js\";\nimport { sha256File, chunkId, yieldToEventLoop } from \"../indexer/helpers.js\";\nimport { walkVaultMdFiles } from \"./walk.js\";\nimport { deduplicateByInode } from \"./deduplicate.js\";\nimport { parseLinks } from \"./parse-links.js\";\nimport { buildNameIndex } from \"./name-index.js\";\nimport { resolveWikilink } from \"./resolve.js\";\nimport type { VaultIndexResult } from \"./types.js\";\n\n/** Number of files to process before yielding to the event loop. */\nconst VAULT_YIELD_EVERY = 1;\n\n/**\n * Index an entire Obsidian vault (or markdown knowledge base) using the\n * async StorageBackend interface.\n *\n * Steps:\n * 1. Walk vault root, following symlinks.\n * 2. Deduplicate by inode — each unique file is indexed once.\n * 3. Build a name index for wikilink resolution.\n * 4. For each canonical file:\n * a. SHA-256 hash for change detection — skip unchanged files.\n * b. Read content, chunk with chunkMarkdown().\n * c. Insert chunks into backend (memory_chunks and memory_fts).\n * d. Upsert vault_files row.\n * 5. Record aliases in vault_aliases.\n * 6. Rebuild vault_name_index table.\n * 7. Rebuild vault_links:\n * a. Parse [[wikilinks]] from each canonical file.\n * b. Resolve each link with resolveWikilink().\n * c. Insert into vault_links.\n * 8. Compute and upsert health metrics (vault_health).\n * 9. Return statistics.\n *\n * @param backend StorageBackend to write to.\n * @param vaultProjectId Registry project ID for the vault \"project\".\n * @param vaultRoot Absolute path to the vault root directory.\n */\nexport async function indexVault(\n backend: StorageBackend,\n vaultProjectId: number,\n vaultRoot: string,\n): Promise<VaultIndexResult> {\n const startTime = Date.now();\n\n const result: VaultIndexResult = {\n filesIndexed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n aliasesRecorded: 0,\n linksExtracted: 0,\n deadLinksFound: 0,\n orphansFound: 0,\n elapsed: 0,\n };\n\n // Step 1: Walk vault, collecting all .md files (follows symlinks)\n const allFiles = walkVaultMdFiles(vaultRoot);\n\n // Step 2: Deduplicate by inode\n const inodeGroups = deduplicateByInode(allFiles);\n\n // Step 3: Build name index (from all files including aliases, for resolution)\n const nameIndex = buildNameIndex(allFiles);\n\n // Step 4: Index each canonical file\n await yieldToEventLoop();\n let filesSinceYield = 0;\n\n for (const group of inodeGroups) {\n if (filesSinceYield >= VAULT_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const { canonical } = group;\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n result.filesSkipped++;\n continue;\n }\n\n const hash = fileContentHash(content);\n\n // Change detection: skip if hash is unchanged\n const existing = await backend.getVaultFile(canonical.vaultRelPath);\n if (existing?.hash === hash) {\n result.filesSkipped++;\n continue;\n }\n\n // Delete old chunks for this vault path\n await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);\n\n // Chunk the content\n const chunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n // Extract title from first H1 heading or filename\n const titleMatch = /^#\\s+(.+)$/m.exec(content);\n const title = titleMatch\n ? titleMatch[1]!.trim()\n : basename(canonical.vaultRelPath, \".md\");\n\n // Build chunk rows\n const chunkRows: ChunkRow[] = [];\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(\n vaultProjectId,\n canonical.vaultRelPath,\n i,\n chunk.startLine,\n chunk.endLine,\n );\n chunkRows.push({\n id,\n projectId: vaultProjectId,\n source: \"vault\",\n tier: \"topic\",\n path: canonical.vaultRelPath,\n startLine: chunk.startLine,\n endLine: chunk.endLine,\n hash: chunk.hash,\n text: chunk.text,\n updatedAt,\n });\n }\n\n if (chunkRows.length > 0) {\n await backend.insertChunks(chunkRows);\n }\n\n // Upsert vault file record\n const vaultFileRow: VaultFileRow = {\n vaultPath: canonical.vaultRelPath,\n inode: canonical.inode,\n device: canonical.device,\n hash,\n title,\n indexedAt: updatedAt,\n };\n await backend.upsertVaultFile(vaultFileRow);\n\n result.filesIndexed++;\n result.chunksCreated += chunks.length;\n }\n\n // Step 5: Record aliases in vault_aliases\n await yieldToEventLoop();\n\n const allAliases: VaultAliasRow[] = [];\n for (const group of inodeGroups) {\n for (const alias of group.aliases) {\n allAliases.push({\n vaultPath: alias.vaultRelPath,\n canonicalPath: group.canonical.vaultRelPath,\n inode: alias.inode,\n device: alias.device,\n });\n result.aliasesRecorded++;\n }\n }\n\n const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));\n for (const canonPath of canonicalPaths) {\n await backend.deleteVaultAliases(canonPath);\n }\n if (allAliases.length > 0) {\n await backend.upsertVaultAliases(allAliases);\n }\n\n // Step 6: Rebuild vault_name_index\n await yieldToEventLoop();\n\n const nameEntries: VaultNameEntry[] = [];\n for (const [name, paths] of nameIndex) {\n for (const path of paths) {\n nameEntries.push({ name, vaultPath: path });\n }\n }\n await backend.replaceNameIndex(nameEntries);\n\n // Step 7: Rebuild vault_links\n await yieldToEventLoop();\n\n const linkRows: VaultLinkRow[] = [];\n const allSourcePaths: string[] = [];\n\n let linkParseYield = 0;\n for (const group of inodeGroups) {\n if (linkParseYield++ % VAULT_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const { canonical } = group;\n allSourcePaths.push(canonical.vaultRelPath);\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n continue;\n }\n\n const parsedLinks = parseLinks(content);\n for (const link of parsedLinks) {\n const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);\n let linkType: string;\n if (link.isMdLink) {\n linkType = link.isEmbed ? \"md-embed\" : \"md-link\";\n } else {\n linkType = link.isEmbed ? \"embed\" : \"wikilink\";\n }\n linkRows.push({\n sourcePath: canonical.vaultRelPath,\n targetRaw: link.raw,\n targetPath: target,\n linkType,\n lineNumber: link.lineNumber,\n confidence: \"EXTRACTED\",\n });\n }\n }\n\n // Replace all links for all sources in batches of 500\n const LINK_BATCH_SIZE = 500;\n for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {\n const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);\n const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));\n await backend.replaceLinksForSources(batchSources, batchLinks);\n await yieldToEventLoop();\n }\n\n result.linksExtracted = linkRows.length;\n result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;\n\n // Step 8: Compute and upsert vault_health metrics\n await yieldToEventLoop();\n\n const outboundMap = new Map<string, number>();\n const deadMap = new Map<string, number>();\n const inboundMap = new Map<string, number>();\n\n for (const row of linkRows) {\n outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);\n if (row.targetPath === null) {\n deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);\n } else {\n inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);\n }\n }\n\n const computedAt = Date.now();\n let orphanCount = 0;\n\n const HEALTH_BATCH_SIZE = 500;\n for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {\n const batch = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE);\n const healthRows: VaultHealthRow[] = batch.map((group) => {\n const path = group.canonical.vaultRelPath;\n const inbound = inboundMap.get(path) ?? 0;\n const outbound = outboundMap.get(path) ?? 0;\n const dead = deadMap.get(path) ?? 0;\n const isOrphan = inbound === 0;\n if (isOrphan) orphanCount++;\n return {\n vaultPath: path,\n inboundCount: inbound,\n outboundCount: outbound,\n deadLinkCount: dead,\n isOrphan,\n computedAt,\n };\n });\n await backend.upsertVaultHealth(healthRows);\n await yieldToEventLoop();\n }\n\n result.orphansFound = orphanCount;\n result.elapsed = Date.now() - startTime;\n\n return result;\n}\n","/**\n * context-handover-prompt.ts — prompt template for the threshold-triggered\n * pre-compaction handover.\n *\n * This is NOT the session-note summary (session-summary-prompt.ts already\n * does that well, and Claude Code's own native compaction summary is also\n * genuinely detailed on file paths and issue numbers). This exists for the\n * one thing neither of those reliably carries: the reasoning behind what\n * happened, which compresses to bullets or vanishes outright once a native\n * compaction fires.\n *\n * FRAMING (lifted from a working example of this exact mechanism, read\n * before writing this prompt): the handover is a DIFF against everything\n * already durable, not a summary of the session. The successor can read\n * git, the tracker, and the notes — so this must not repeat what is\n * recoverable there. What cannot be recovered is what only ever existed in\n * the conversation: reasoning, rejected alternatives, negative results and\n * what they cost, traps discovered but invisible in the repo, and specific\n * values/ids/paths that would otherwise have to be dug up again.\n *\n * Length is not a target. A short session earns a short handover; every\n * line in a long one must be something the successor could not have\n * obtained anywhere else.\n */\n\nexport interface HandoverPromptParams {\n /** Interleaved user + assistant turns, oldest first, each prefixed with\n * who said it — this prompt needs the assistant's reasoning, not just\n * what the user asked for. */\n turns: string[];\n /** Git log output for the session period, if any. */\n gitLog: string;\n cwd: string;\n /** The previous cached handover's summary text, if this session already\n * produced one (the warmup handover, when this call is the refresh).\n * Passed through so the new handover can carry it forward rather than\n * silently dropping everything it already captured — see the \"pointer\n * to the previous handover, kept rather than superseded\" structure. */\n previousHandover?: string;\n}\n\nexport function buildContextHandoverPrompt(params: HandoverPromptParams): string {\n const { turns, gitLog, cwd, previousHandover } = params;\n\n const turnsSection = turns.length > 0\n ? turns.join(\"\\n\\n\")\n : \"(No turns extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const previousSection = previousHandover\n ? `\\nA PREVIOUS HANDOVER already exists for this session (from an earlier, \\\nlower threshold). Carry it forward — start your response with a one-line \\\npointer to it (\"Carries forward from the previous handover, which stays for \\\nits detail.\") and then write only what has happened SINCE it that a compact \\\nwould also lose. Do not re-derive or repeat what it already covered.\\n\\n\\\nPREVIOUS HANDOVER:\\n${previousHandover}\\n`\n : \"\";\n\n return `This session is approaching a context-window compaction. Write the \\\nhandover that would let a successor continue WITHOUT re-reading this \\\nconversation and WITHOUT asking the user to repeat themselves.\n\nProject directory: ${cwd}\n${previousSection}\nSCOPING RULE — the single most important instruction here: this is a DIFF \\\nagainst everything already durable, not a summary of the session. Assume \\\nthe successor can and will read git history, the issue tracker, and any \\\nnotes files themselves. Do NOT restate anything recoverable there — no file \\\ninventories, no \"implemented X\" restatements, no chronology of commands run. \\\nWrite down ONLY what existed exclusively in this conversation and would \\\notherwise be gone: the reasoning, not the result.\n\nWrite ONLY the sections below that this session actually has content for — \\\nomit a section entirely rather than writing \"none\" or padding it:\n\nWHERE THIS SESSION ENDED — the concrete state right now: what is verified \\\nworking (tests green, tree clean, etc.) versus merely attempted, any branch \\\nor working-tree state that isn't obvious from git status alone (e.g. a \\\ndetached HEAD, an intentionally uncommitted change, a stash).\n\nWHAT CLOSED — each item paired with the evidence that closed it, not just \\\nthe claim that it did.\n\nWHAT IS FILED AND STILL OPEN — each with its next concrete step or who/what \\\nit is waiting on.\n\nDECISIONS & REASONING — choices actually made, each with why this way and \\\nnot another way.\n\nREJECTED ALTERNATIVES — approaches considered and set aside, and why. An \\\nalternative without a reason is not worth recording.\n\nNEGATIVE RESULTS — something investigated that turned out NOT to be the \\\nanswer, kept WITH the reasoning that ruled it out (e.g. \"X looked like the \\\ncause — it wasn't; the actual mechanism was Y\"). Without the reasoning \\\nattached, a successor re-investigates the same dead end.\n\nTRAPS — anything discovered that is invisible from the repo alone and would \\\notherwise bite a successor immediately (a required manual step, an ordering \\\nconstraint, a state that looks fine but isn't).\n\nCRITICAL VALUES — any specific id, path, URL, credential name, version \\\nnumber, or configuration value that appeared in conversation and whose loss \\\nwould force someone to go find it again.\n\nTHE FEW THINGS THAT MATTER MOST, IF NOTHING ELSE IS READ — at most 3 bullets. \\\nForces a priority judgement; do not pad this to fill space.\n\nFormat your response as markdown with those section headers (only the ones \\\nyou have content for), no other structure.\n\n---\n\nCONVERSATION:\n${turnsSection}\n\nGIT COMMITS:\n${gitSection}`;\n}\n","/**\n * context-handover-worker.ts — the threshold-triggered pre-compaction\n * handover.\n *\n * `session-summary-worker.ts` already spawns a model to write session notes,\n * but only from the PreCompact hook itself — which fires at the compaction\n * boundary, far too late to be injected INTO that same compaction's\n * post-compact context. This worker runs earlier, off a token-count\n * threshold (see ../hooks/ts/lib/context-fill.ts), so a model-written\n * handover already exists in a cache file by the time compaction happens.\n *\n * Deliberately narrow: this does not write a session note, does not touch\n * the session-summary cooldown file, and does not run KG extraction. It\n * writes one thing — a cache file the PreCompact hook can read — because its\n * only job is to exist before compaction does, not to duplicate what\n * session-summary-worker already does well.\n *\n * The prompt itself (see templates/context-handover-prompt.ts) treats the\n * handover as a DIFF against everything already durable — git, the tracker,\n * the notes — not a session summary, and a refresh run carries the previous\n * cached handover forward rather than overwriting it.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\n\nimport {\n findLatestJsonl,\n getGitContext,\n spawnSummarizer,\n contentToText,\n} from \"./session-summary-worker.js\";\nimport { buildContextHandoverPrompt } from \"./templates/context-handover-prompt.js\";\nimport {\n readContextHandoverCache,\n writeContextHandoverCache,\n type HandoverThreshold,\n} from \"../hooks/ts/lib/context-handover-cache.js\";\n\nexport type { HandoverThreshold } from \"../hooks/ts/lib/context-handover-cache.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContextHandoverPayload {\n cwd: string;\n sessionId: string;\n transcriptPath?: string;\n threshold: HandoverThreshold;\n /** True when this fired via the \"no time for a clean crossing\" path\n * (see isImmediate in context-fill.ts) — logged, not otherwise acted on. */\n urgent?: boolean;\n}\n\n/** How much of the tail of the transcript to feed the summarizer. Generous —\n * this runs at most twice per session, not on every tool call. */\nconst MAX_TURN_CHARS = 150_000;\n\n/** Max turns to include (a hard floor even if MAX_TURN_CHARS is not hit). */\nconst MAX_TURNS = 120;\n\n// ---------------------------------------------------------------------------\n// Transcript → turns\n// ---------------------------------------------------------------------------\n\n/**\n * Interleaved \"User:\"/\"Assistant:\" turns, oldest-kept-from-the-end so a long\n * transcript is truncated to its most recent content rather than its\n * earliest. Unlike session-summary-worker's extractFromJsonl (which keeps\n * only user messages, because a session note doesn't need the assistant's\n * own reasoning) this keeps BOTH sides — the whole point of this prompt is\n * the assistant's reasoning, which lives in assistant turns.\n */\nfunction extractTurns(jsonlPath: string): string[] {\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch {\n return [];\n }\n\n if (raw.length > MAX_TURN_CHARS) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - MAX_TURN_CHARS);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_TURN_CHARS);\n }\n\n const turns: string[] = [];\n for (const line of raw.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n const text = contentToText(msg?.content);\n if (text && text.length >= 3 && !text.startsWith(\"<system-reminder>\")) {\n turns.push(`User: ${text.slice(0, 2000)}`);\n }\n } else if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n const text = contentToText(msg?.content);\n if (text && text.length >= 3) {\n turns.push(`Assistant: ${text.slice(0, 4000)}`);\n }\n }\n }\n\n return turns.slice(-MAX_TURNS);\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `context-handover` work item.\n *\n * THROWS on every failure path (missing input, no transcript, no turns, an\n * empty/failed summarizer run) rather than logging and returning silently.\n * That used to be a deliberate choice — avoid the work queue's own retry\n * piling up redundant LLM calls — but it made a real failure invisible: a\n * daemon restart mid-spawn (session 77084e72-...) left no cache, no error\n * anywhere the work queue's own stats could show, and a trigger-side marker\n * that (at the time) had already been set to \"done\". Throwing here makes\n * work-queue-worker.ts call markFailed(), which logs prominently AND is\n * visible via `work_queue_stats` / `pai daemon status` — not just a stderr\n * line that can scroll away. It is a second, faster (seconds-to-minutes\n * backoff) retry path alongside the primary one in context-handover-\n * trigger.ts (which retries on its own ~5-minute cadence based on whether a\n * cache actually appeared); the two are complementary, not redundant — a\n * failed cache write costs nothing to retry twice.\n */\nexport async function handleContextHandover(payload: ContextHandoverPayload): Promise<void> {\n const { cwd, sessionId, transcriptPath, threshold, urgent } = payload;\n\n if (!cwd || !sessionId) {\n throw new Error(\"[context-handover] payload missing cwd or sessionId\");\n }\n\n process.stderr.write(\n `[context-handover] Starting for ${cwd} (session=${sessionId}, threshold=${threshold}` +\n `${urgent ? \", urgent\" : \"\"}).\\n`\n );\n\n let jsonlPath: string | null = transcriptPath && existsSync(transcriptPath) ? transcriptPath : null;\n if (!jsonlPath) jsonlPath = findLatestJsonl(cwd);\n if (!jsonlPath) {\n throw new Error(`[context-handover] No transcript found for ${cwd} (session=${sessionId})`);\n }\n\n const turns = extractTurns(jsonlPath);\n if (turns.length === 0) {\n throw new Error(`[context-handover] No turns extracted from ${jsonlPath} (session=${sessionId})`);\n }\n\n const gitLog = await getGitContext(cwd);\n\n // Carry the previous handover forward (see buildContextHandoverPrompt's\n // scoping rule) rather than silently overwriting it — a refresh at the\n // second threshold should not lose what the warmup handover already\n // captured.\n const previous = readContextHandoverCache(sessionId);\n const prompt = buildContextHandoverPrompt({\n turns,\n gitLog,\n cwd,\n previousHandover: previous?.summary,\n });\n process.stderr.write(`[context-handover] Sending ${prompt.length} char prompt to sonnet...\\n`);\n\n const summary = await spawnSummarizer(prompt, \"sonnet\");\n if (!summary || !summary.trim()) {\n // No cache write on this path — an existing cache from an earlier\n // successful run is left untouched rather than clobbered with nothing.\n throw new Error(`[context-handover] sonnet produced no output (session=${sessionId})`);\n }\n\n writeContextHandoverCache({\n sessionId,\n cwd,\n threshold,\n generatedAt: new Date().toISOString(),\n model: \"sonnet\",\n summary: summary.trim(),\n });\n\n process.stderr.write(\n `[context-handover] Wrote cache for session ${sessionId} (${summary.length} chars).\\n`\n );\n}\n","/**\n * topic-detect-worker.ts — Topic shift detection for session note splitting\n *\n * Processes `topic-detect` work items by:\n * 1. Extracting recent user messages from the JSONL transcript\n * 2. Running the BM25-based topic shift detector against the PAI memory DB\n * 3. If a shift is detected, recording a topic boundary marker\n *\n * The actual note splitting is handled by session-summary-worker.ts when it\n * processes the next `session-summary` work item — it uses the TOPIC: line\n * from the summarizer to decide whether to create a new note.\n *\n * This worker provides an additional signal: project-level topic shift\n * (e.g., conversation moved from project A to project B). The session\n * summary worker handles intra-project topic shifts (e.g., from \"dark mode\"\n * to \"keyboard IPC\" within the same project).\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { detectTopicShift } from \"../topics/detector.js\";\nimport { registryBackend, storageBackend } from \"./daemon/state.js\";\nimport {\n findNotesDir,\n getCurrentNotePath,\n appendCheckpoint,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectPayload {\n /** Working directory of the session. */\n cwd: string;\n /** Recent conversation context (extracted user messages). */\n context?: string;\n /** The project slug the session is currently routed to. */\n currentProject?: string;\n /** Path to the JSONL transcript (optional — used to extract context if not provided). */\n transcriptPath?: string;\n /** Session ID (for logging). */\n sessionId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL context extraction (lightweight — just last few user messages)\n// ---------------------------------------------------------------------------\n\nconst MAX_CONTEXT_MESSAGES = 5;\nconst MAX_CONTEXT_CHARS = 2000;\n\n/**\n * Extract recent user messages from a JSONL transcript for topic detection.\n * Takes only the last few messages to represent the current topic.\n */\nfunction extractRecentContext(jsonlPath: string): string {\n try {\n const raw = readFileSync(jsonlPath, \"utf-8\");\n // Read from the end — last 50KB should be more than enough\n const tail = raw.length > 50_000 ? raw.slice(-50_000) : raw;\n const lines = tail.trim().split(\"\\n\");\n\n const messages: string[] = [];\n\n for (let i = lines.length - 1; i >= 0 && messages.length < MAX_CONTEXT_MESSAGES; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n\n try {\n const entry = JSON.parse(line) as Record<string, unknown>;\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && text.length > 3) {\n messages.unshift(text.slice(0, 500));\n }\n }\n }\n } catch { /* skip invalid JSON */ }\n }\n\n return messages.join(\"\\n\\n\").slice(0, MAX_CONTEXT_CHARS);\n } catch {\n return \"\";\n }\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Topic boundary file — signals to session-summary-worker\n// ---------------------------------------------------------------------------\n\nconst TOPIC_BOUNDARY_FILE = \"topic-boundary.json\";\n\ninterface TopicBoundary {\n timestamp: string;\n previousProject: string | null;\n suggestedProject: string | null;\n confidence: number;\n context: string;\n}\n\n/**\n * Write a topic boundary marker into the Notes directory.\n * The session-summary-worker checks for this file and uses it as an\n * additional signal that a new note should be created.\n */\nfunction writeTopicBoundary(\n cwd: string,\n boundary: TopicBoundary\n): void {\n try {\n const notesInfo = findNotesDir(cwd);\n const boundaryPath = join(notesInfo.path, TOPIC_BOUNDARY_FILE);\n writeFileSync(boundaryPath, JSON.stringify(boundary, null, 2), \"utf-8\");\n process.stderr.write(\n `[topic-detect] Wrote topic boundary marker: ${boundaryPath}\\n`\n );\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not write boundary marker: ${e}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `topic-detect` work item.\n *\n * Called by work-queue-worker.ts. Throws on fatal errors so the work queue\n * retry logic handles them.\n */\nexport async function handleTopicDetect(payload: TopicDetectPayload): Promise<void> {\n const { cwd, currentProject, transcriptPath, sessionId } = payload;\n\n if (!cwd) {\n throw new Error(\"topic-detect payload missing cwd\");\n }\n\n process.stderr.write(\n `[topic-detect] Starting for ${cwd}` +\n `${currentProject ? ` (project=${currentProject})` : \"\"}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}\\n`\n );\n\n // Check that daemon state is available\n if (!registryBackend || !storageBackend) {\n process.stderr.write(\n \"[topic-detect] Registry backend or storage backend not available — skipping.\\n\"\n );\n return;\n }\n\n // Extract context from payload or transcript\n let context = payload.context || \"\";\n\n if (!context && transcriptPath && existsSync(transcriptPath)) {\n context = extractRecentContext(transcriptPath);\n }\n\n if (!context || context.trim().length < 10) {\n process.stderr.write(\n \"[topic-detect] Insufficient context for topic detection — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[topic-detect] Context: ${context.length} chars, checking against memory...\\n`\n );\n\n // Run the BM25-based topic shift detector\n const result = await detectTopicShift(registryBackend, storageBackend, {\n context,\n currentProject,\n threshold: 0.6,\n candidates: 20,\n });\n\n process.stderr.write(\n `[topic-detect] Result: shifted=${result.shifted}, ` +\n `suggested=${result.suggestedProject}, confidence=${result.confidence.toFixed(2)}, ` +\n `chunks=${result.chunkCount}\\n`\n );\n\n if (result.topProjects.length > 0) {\n process.stderr.write(\n `[topic-detect] Top projects: ${result.topProjects.map(\n (p) => `${p.slug}(${(p.score * 100).toFixed(0)}%)`\n ).join(\", \")}\\n`\n );\n }\n\n if (result.shifted) {\n // Record the topic boundary\n writeTopicBoundary(cwd, {\n timestamp: new Date().toISOString(),\n previousProject: result.currentProject,\n suggestedProject: result.suggestedProject,\n confidence: result.confidence,\n context: context.slice(0, 200),\n });\n\n // Also append a checkpoint to the current session note\n try {\n const notesInfo = findNotesDir(cwd);\n const notePath = getCurrentNotePath(notesInfo.path);\n if (notePath) {\n appendCheckpoint(\n notePath,\n `Topic shift detected: conversation moved from **${result.currentProject}** ` +\n `to **${result.suggestedProject}** (confidence: ${(result.confidence * 100).toFixed(0)}%). ` +\n `A new session note will be created for the new topic.`\n );\n }\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not append checkpoint: ${e}\\n`);\n }\n }\n\n process.stderr.write(\"[topic-detect] Done.\\n\");\n}\n","/**\n * work-queue-worker.ts — Daemon worker loop for the persistent work queue\n *\n * Runs every 5 seconds to drain the queue.\n * Handles 'session-end' work items by reading the transcript, extracting\n * work summaries, updating the session note, and updating TODO.md.\n * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.\n * Handles 'context-handover' items by spawning Sonnet to write the\n * threshold-triggered pre-compaction reasoning handover (see\n * context-handover-worker.ts and hooks/ts/lib/context-fill.ts).\n * Handles 'registry-scan' items by running performScan() against the registry DB.\n *\n * Handles 'topic-detect' items by running BM25-based topic shift detection.\n * Other item types (note-update, todo-update) are stubs — they log and\n * complete immediately, ready for future expansion.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\n\nimport {\n dequeue,\n enqueue,\n markCompleted,\n markFailed,\n cleanup,\n getStats,\n hasPendingOrProcessingOfType,\n type WorkItem,\n} from \"./work-queue.js\";\n\nimport {\n handleSessionSummary,\n type SessionSummaryPayload,\n} from \"./session-summary-worker.js\";\n\nimport {\n handleContextHandover,\n type ContextHandoverPayload,\n} from \"./context-handover-worker.js\";\n\nimport {\n handleTopicDetect,\n type TopicDetectPayload,\n} from \"./topic-detect-worker.js\";\n\n// Registry scan — called from handleRegistryScan()\nimport { performScan } from \"../cli/commands/registry/scan.js\";\nimport { registryBackend } from \"./daemon/state.js\";\n\n// Hooks lib imports — resolving through the compiled JS path.\n// These are the same utilities used by stop-hook.ts.\nimport {\n findNotesDir,\n getCurrentNotePath,\n addWorkToSessionNote,\n finalizeSessionNote,\n updateTodoContinue, sessionIdFromTranscript,\n archiveSessionFilesToSessionsDir,\n type WorkItem as NoteWorkItem,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst WORKER_INTERVAL_MS = 5_000;\nconst HOUSEKEEPING_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes\n\n// ---------------------------------------------------------------------------\n// Timers (stored so shutdown can clear them)\n// ---------------------------------------------------------------------------\n\nlet workerTimer: ReturnType<typeof setInterval> | null = null;\nlet housekeepingTimer: ReturnType<typeof setInterval> | null = null;\nlet _immediateSignal = false;\n\n// ---------------------------------------------------------------------------\n// Worker loop\n// ---------------------------------------------------------------------------\n\n/** Start the background worker and housekeeping timers. */\nexport function startWorker(): void {\n process.stderr.write(\"[work-queue-worker] Starting worker loop.\\n\");\n\n workerTimer = setInterval(async () => {\n try {\n await processNextItem();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Uncaught error in worker loop: ${e}\\n`);\n }\n }, WORKER_INTERVAL_MS);\n\n housekeepingTimer = setInterval(() => {\n try {\n cleanup();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Housekeeping error: ${e}\\n`);\n }\n }, HOUSEKEEPING_INTERVAL_MS);\n\n process.stderr.write(\"[work-queue-worker] Worker started (interval=5s, housekeeping=10min).\\n\");\n}\n\n/** Stop the worker timers gracefully. */\nexport function stopWorker(): void {\n if (workerTimer !== null) {\n clearInterval(workerTimer);\n workerTimer = null;\n }\n if (housekeepingTimer !== null) {\n clearInterval(housekeepingTimer);\n housekeepingTimer = null;\n }\n process.stderr.write(\"[work-queue-worker] Worker stopped.\\n\");\n}\n\n/**\n * Signal that new work has been enqueued.\n * The worker will run on its next tick — we don't need to reset the timer\n * since 5 s is fast enough. The flag allows future optimisations.\n */\nexport function notifyNewWork(): void {\n _immediateSignal = true;\n}\n\n// ---------------------------------------------------------------------------\n// Item processor (sequential — one item per tick)\n// ---------------------------------------------------------------------------\n\nasync function processNextItem(): Promise<void> {\n const item = dequeue();\n if (!item) return;\n\n process.stderr.write(\n `[work-queue-worker] Processing ${item.type} (id=${item.id}, attempt=${item.attempts}).\\n`\n );\n\n try {\n switch (item.type) {\n case \"session-end\":\n await handleSessionEnd(item);\n break;\n\n case \"session-summary\":\n await handleSessionSummary(item.payload as unknown as SessionSummaryPayload);\n break;\n\n case \"context-handover\":\n await handleContextHandover(item.payload as unknown as ContextHandoverPayload);\n break;\n\n case \"topic-detect\":\n await handleTopicDetect(item.payload as unknown as TopicDetectPayload);\n break;\n\n case \"registry-scan\":\n await handleRegistryScan();\n break;\n\n case \"note-update\":\n case \"todo-update\":\n // Stubs — log and complete\n process.stderr.write(\n `[work-queue-worker] Item type '${item.type}' is not yet implemented — completing as no-op.\\n`\n );\n break;\n\n default:\n throw new Error(`Unknown work item type: ${(item as WorkItem).type}`);\n }\n\n markCompleted(item.id);\n process.stderr.write(`[work-queue-worker] Completed ${item.type} (id=${item.id}).\\n`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n markFailed(item.id, msg);\n }\n}\n\n// ---------------------------------------------------------------------------\n// session-end handler\n// ---------------------------------------------------------------------------\n\n/**\n * Process a 'session-end' work item.\n *\n * Expected payload:\n * transcriptPath: string — absolute path to the .jsonl transcript\n * cwd: string — working directory of the session\n * message?: string — COMPLETED: line extracted by the hook (optional)\n */\nasync function handleSessionEnd(item: WorkItem): Promise<void> {\n const { transcriptPath, cwd, message: hookMessage } = item.payload as {\n transcriptPath: string;\n cwd: string;\n message?: string;\n };\n\n if (!transcriptPath) throw new Error(\"session-end payload missing transcriptPath\");\n if (!cwd) throw new Error(\"session-end payload missing cwd\");\n\n // Read transcript\n let transcript: string;\n try {\n transcript = readFileSync(transcriptPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read transcript at ${transcriptPath}: ${e}`);\n }\n\n const lines = transcript.trim().split(\"\\n\");\n\n // Extract work items from transcript\n const workItems = extractWorkFromTranscript(lines);\n\n // Determine completion message\n let message = hookMessage ?? \"\";\n if (!message) {\n const lastEntry = tryParseJson(lines[lines.length - 1]) as\n | { type?: string; message?: { content?: unknown } }\n | null;\n if (lastEntry?.type === \"assistant\" && lastEntry.message?.content) {\n const content = contentToText(lastEntry.message.content);\n const m = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (m) {\n message = m[1].trim().replace(/\\*+/g, \"\").replace(/\\[.*?\\]/g, \"\").trim();\n }\n }\n }\n\n // Find notes directory and current note\n const notesInfo = findNotesDir(cwd);\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n // Add work items to session note\n if (workItems.length > 0) {\n addWorkToSessionNote(currentNotePath, workItems);\n process.stderr.write(\n `[work-queue-worker] Added ${workItems.length} work item(s) to note.\\n`\n );\n } else if (message) {\n addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);\n process.stderr.write(\"[work-queue-worker] Added completion message to note.\\n\");\n }\n\n // Finalize the note\n const summary = message || \"Session completed.\";\n finalizeSessionNote(currentNotePath, summary);\n process.stderr.write(\n `[work-queue-worker] Finalized session note: ${basename(currentNotePath)}.\\n`\n );\n\n // Update TODO.md ## Continue section.\n //\n // A session that produced neither work items nor a completion message has\n // no handover to write. Claiming the slot anyway hands the next session a\n // block naming a session that did nothing — see the 2026-08-04 incident\n // recorded in AIBroker's Notes. applyContinue now refuses such a write on\n // its own; this skips building it in the first place, so the \"last session\"\n // line keeps pointing at the last session that actually did something.\n if (workItems.length === 0 && !message) {\n process.stderr.write(\n \"[work-queue-worker] Nothing to hand over — leaving TODO.md ## Continue intact.\\n\"\n );\n } else {\n try {\n const stateLines: string[] = [];\n stateLines.push(`Working directory: ${cwd}`);\n if (workItems.length > 0) {\n stateLines.push(\"\", \"Work completed:\");\n for (const wi of workItems.slice(0, 5)) {\n stateLines.push(`- ${wi.title}`);\n }\n }\n if (message) {\n stateLines.push(\"\", `Last completed: ${message}`);\n }\n updateTodoContinue(\n cwd,\n basename(currentNotePath),\n stateLines.join(\"\\n\"),\n \"session-end\",\n // Same reasoning as the stop hook: the transcript filename is the\n // session's real identity, and the note title is not.\n sessionIdFromTranscript(transcriptPath)\n );\n } catch (todoError) {\n // Non-fatal — log and continue\n process.stderr.write(\n `[work-queue-worker] Could not update TODO.md: ${todoError}\\n`\n );\n }\n }\n } else {\n process.stderr.write(\n \"[work-queue-worker] No current session note found — skipping note update.\\n\"\n );\n }\n\n // Move session .jsonl files to sessions/ subdirectory\n try {\n const transcriptDir = dirname(transcriptPath);\n const archivedCount = archiveSessionFilesToSessionsDir(transcriptDir);\n if (archivedCount > 0) {\n process.stderr.write(\n `[work-queue-worker] Archived ${archivedCount} session file(s) to sessions/.\\n`\n );\n }\n } catch (moveError) {\n // Non-fatal\n process.stderr.write(`[work-queue-worker] Could not move session files: ${moveError}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// registry-scan handler\n// ---------------------------------------------------------------------------\n\n/**\n * Run performScan() against the registry DB.\n *\n * Debounce: if another registry-scan job is already pending in the queue,\n * we still run this one (dequeue already picked it), but enqueueRegistryScan()\n * checks before enqueuing so duplicates rarely reach here.\n */\nasync function handleRegistryScan(): Promise<void> {\n if (!registryBackend) {\n throw new Error(\"registry-scan: registryBackend not initialized yet\");\n }\n\n const t0 = Date.now();\n process.stderr.write(\"[work-queue-worker] Running registry scan...\\n\");\n\n const result = await performScan();\n const elapsed = Date.now() - t0;\n\n process.stderr.write(\n `[work-queue-worker] Registry scan complete: ` +\n `${result.projectsScanned} projects (${result.projectsNew} new, ${result.projectsUpdated} updated), ` +\n `${result.sessionsScanned} sessions (${result.sessionsNew} new) in ${elapsed}ms.\\n`\n );\n\n if (result.skipped.length > 0) {\n process.stderr.write(\n `[work-queue-worker] Registry scan: ${result.skipped.length} project(s) skipped (path not found).\\n`\n );\n }\n}\n\n/**\n * Enqueue a registry-scan work item — debounced.\n * If a registry-scan item is already pending or processing, skip enqueue.\n */\nexport function enqueueRegistryScan(): void {\n // Debounce: skip if already queued or processing\n if (hasPendingOrProcessingOfType(\"registry-scan\")) {\n process.stderr.write(\"[work-queue-worker] Registry scan already pending/processing — skipping duplicate enqueue.\\n\");\n return;\n }\n\n enqueue({\n type: \"registry-scan\",\n priority: 5, // lowest priority — runs after session-end, session-summary\n payload: {},\n });\n}\n\n// ---------------------------------------------------------------------------\n// Transcript parsing helpers (mirrors stop-hook.ts logic)\n// ---------------------------------------------------------------------------\n\nfunction tryParseJson(line: string): Record<string, unknown> | null {\n try {\n return JSON.parse(line) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\nfunction extractWorkFromTranscript(lines: string[]): NoteWorkItem[] {\n const workItems: NoteWorkItem[] = [];\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n const entry = tryParseJson(line);\n if (!entry || entry.type !== \"assistant\") continue;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n if (!msg?.content) continue;\n\n const content = contentToText(msg.content);\n\n // SUMMARY: line (preferred)\n const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const summary = summaryMatch[1].trim();\n if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n seenSummaries.add(summary);\n\n const details: string[] = [];\n const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1]\n .split(\"\\n\")\n .map((l) =>\n l.replace(/^[-*•]\\s*/, \"\").replace(/^\\d+\\.\\s*/, \"\").trim()\n )\n .filter((l) => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n\n workItems.push({\n title: summary,\n details: details.length > 0 ? details : undefined,\n completed: true,\n });\n }\n }\n\n // COMPLETED: line (fallback)\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch && workItems.length === 0) {\n const completed = completedMatch[1]\n .trim()\n .replace(/\\*+/g, \"\")\n .replace(/\\[.*?\\]/g, \"\")\n .trim();\n if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n seenSummaries.add(completed);\n workItems.push({ title: completed, completed: true });\n }\n }\n }\n\n return workItems;\n}\n","/**\n * Index, embed, vault index, and registry scan schedulers for the PAI daemon.\n * Exports run* functions (also called on-demand by the IPC handler)\n * and the start* functions invoked once at daemon startup.\n */\n\nimport { readWorkersSection } from \"../../workers/config.js\";\nimport { keepaliveSecs, runKeepaliveBeat, type BeatMetrics } from \"../../workers/keepalive.js\";\nimport { workersLogDir } from \"../../workers/paths.js\";\nimport { runSupervisionTick, stallMinutesFromEnv, thrashFailsFromEnv } from \"../../workers/supervision.js\";\nimport { runSessionKeepaliveTick } from \"../session-keepalive.js\";\n// Static on purpose: a running daemon must never need a file from dist/ again.\n// tsdown emits content-hashed chunks and prune-dist.mjs deletes superseded ones,\n// so a lazy import() first hit after a rebuild throws \"Cannot find module\".\nimport { embedChunksWithBackend } from \"../../memory/indexer-backend.js\";\nimport { indexVault } from \"../../memory/vault-indexer.js\";\nimport { enqueueRegistryScan } from \"../work-queue-worker.js\";\nimport { PassSpawnError, runPassChild, type PassName } from \"../pass-priority.js\";\nimport {\n registryBackend,\n storageBackend,\n daemonConfig,\n indexInProgress,\n embedInProgress,\n vaultIndexInProgress,\n lastVaultIndexTime,\n passFailures,\n shutdownRequested,\n setIndexInProgress,\n setLastIndexTime,\n setIndexSchedulerTimer,\n setEmbedInProgress,\n setLastEmbedTime,\n setEmbedSchedulerTimer,\n setVaultIndexInProgress,\n setLastVaultIndexTime,\n setCacheKeepaliveTimer,\n} from \"./state.js\";\n\n// ---------------------------------------------------------------------------\n// Failed-pass bookkeeping\n// ---------------------------------------------------------------------------\n\n/** Wait before retry 1, 2, 3 of a failed pass: three retries over 30 minutes. */\nexport const PASS_RETRY_DELAYS_MS = [5 * 60_000, 10 * 60_000, 15 * 60_000];\n\nconst DIST_CHANGED_HINT =\n \"dist changed under the running daemon: run `pai daemon restart`\";\n\n/**\n * Record a failed scheduled pass in the daemon status and retry it with\n * backoff. One log line was all a crashed embed pass left behind, and nobody\n * read it for 7 hours; `pai daemon status` now shows it until a pass succeeds.\n */\nfunction failPass(name: string, e: unknown, retry: () => Promise<void>): void {\n let error = e instanceof Error ? e.message : String(e);\n if (/Cannot find module .*dist\\//.test(error)) error += ` — ${DIST_CHANGED_HINT}`;\n const attempts = (passFailures.get(name)?.attempts ?? 0) + 1;\n const delay = PASS_RETRY_DELAYS_MS[attempts - 1];\n passFailures.set(name, { at: Date.now(), error, attempts, gaveUp: delay === undefined });\n process.stderr.write(\n `[pai-daemon] ${name} pass FAILED (attempt ${attempts}): ${error}\\n` +\n (delay === undefined\n ? `[pai-daemon] ${name} pass: giving up until the next scheduled run\\n`\n : `[pai-daemon] ${name} pass: retrying in ${Math.round(delay / 60_000)} min\\n`)\n );\n if (delay === undefined) return;\n const t = setTimeout(() => void retry().catch(() => {}), delay);\n if (t.unref) t.unref();\n}\n\n/**\n * Run a heavy pass at background priority in a child process (passPriority\n * \"background\"), else in-process. Falls back in-process, with a warning, only\n * when the child cannot be spawned; a child that ran and failed is a failure.\n */\nasync function runPassPrioritised<T>(name: PassName, inProcess: () => Promise<T>): Promise<T> {\n if (daemonConfig.passPriority !== \"background\") return inProcess();\n try {\n return await runPassChild<T>(name);\n } catch (e) {\n if (!(e instanceof PassSpawnError)) throw e;\n process.stderr.write(`[pai-daemon] ${name} pass: ${e.message}; running in-process at normal priority\\n`);\n return inProcess();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Index scheduler\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between vault index runs (30 minutes). */\nconst VAULT_INDEX_MIN_INTERVAL_MS = 30 * 60 * 1000;\n\n/**\n * Run a full index pass. Guards against overlapping runs with indexInProgress.\n * Called both by the scheduler and by the index_now IPC method.\n */\nexport async function runIndex(): Promise<void> {\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index already in progress, skipping.\\n\");\n return;\n }\n\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed in progress, deferring index run.\\n\");\n return;\n }\n\n setIndexInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled index run...\\n\");\n\n const { projects, result } = await runPassPrioritised(\"index\", () =>\n storageBackend.indexAll(registryBackend)\n );\n const elapsed = Date.now() - t0;\n setLastIndexTime(Date.now());\n passFailures.delete(\"index\");\n process.stderr.write(\n `[pai-daemon] Index complete: ${projects} projects, ` +\n `${result.filesProcessed} files, ${result.chunksCreated} chunks ` +\n `(${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Index error: ${msg}\\n`);\n failPass(\"index\", e, runIndex);\n } finally {\n setIndexInProgress(false);\n }\n}\n\n/**\n * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.\n * Skips if no vaultPath is configured, or if project index/embed is in progress.\n */\nexport async function runVaultIndex(): Promise<void> {\n if (!daemonConfig.vaultPath) return;\n\n if (vaultIndexInProgress) {\n process.stderr.write(\"[pai-daemon] Vault index already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Index/embed in progress, deferring vault index.\\n\");\n return;\n }\n\n if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) {\n return;\n }\n\n let vaultProjectId = daemonConfig.vaultProjectId;\n if (!vaultProjectId) {\n const row = await registryBackend.getProjectByRootPath(daemonConfig.vaultPath!);\n vaultProjectId = row?.id ?? 999;\n if (!row) {\n process.stderr.write(\"[pai-daemon] Vault not in project registry — using synthetic project ID 999.\\n\");\n }\n }\n\n setVaultIndexInProgress(true);\n const t0 = Date.now();\n\n process.stderr.write(\"[pai-daemon] Starting vault index run...\\n\");\n\n try {\n const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath!);\n const elapsed = Date.now() - t0;\n setLastVaultIndexTime(Date.now());\n passFailures.delete(\"vault\");\n process.stderr.write(\n `[pai-daemon] Vault index complete: ${r.filesIndexed} files, ` +\n `${r.linksExtracted} links, ${r.deadLinksFound} dead, ` +\n `${r.orphansFound} orphans (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Vault index error: ${msg}\\n`);\n failPass(\"vault\", e, runVaultIndex);\n } finally {\n setVaultIndexInProgress(false);\n }\n}\n\n/**\n * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.\n */\nexport function startIndexScheduler(): void {\n const intervalMs = daemonConfig.indexIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\\n`\n );\n\n // Indexing and embedding are mutually exclusive (each defers to the other),\n // and an index pass regularly outlasts the index interval. Left to two\n // independent timers, the embed tick therefore lands while indexing is in\n // progress and is dropped — observed live on 2026-08-01 as six consecutive\n // index passes with no embed pass at all, and a 143k-chunk embedding backlog.\n // Chaining embed onto the end of every index pass makes the alternation\n // guaranteed rather than a race: index, then vault, then embed, every cycle.\n const cycle = (label: string, withEmbed: boolean) =>\n runIndex()\n .then(() => runVaultIndex())\n .then(() => (withEmbed ? runEmbed() : undefined))\n .catch((e) => {\n process.stderr.write(`[pai-daemon] ${label} index error: ${e}\\n`);\n });\n\n // The startup pass indexes but must NOT embed unless embedOnStartup says so.\n // startEmbedScheduler already refuses a startup embed for exactly that reason;\n // chaining runEmbed onto this pass reinstated the same CPU storm through the\n // back door. Seen live after a reboot: \"Startup embed pass skipped\n // (embedOnStartup=false)\" in the log, and minutes later the same boot running\n // a 5000-chunk pass at ~450% CPU. A guard one scheduler honours and another\n // walks around is not a guard.\n // With a maintenance hour configured there is no startup pass at all. Every\n // restart otherwise buys itself a full index run, so on a machine that is\n // rebooted or has the daemon reloaded during the day the \"maintenance runs at\n // night\" guarantee is worth nothing: the heavy work simply follows the\n // restarts around. Indexing waits for the window like everything else.\n if (daemonConfig.maintenanceHour === undefined) {\n setTimeout(() => void cycle(\"Startup\", daemonConfig.embedOnStartup), 2_000);\n } else {\n process.stderr.write(\n \"[pai-daemon] Startup index pass skipped (maintenanceHour is set).\\n\"\n );\n }\n\n // Anchor the recurring cycle to a wall-clock hour when one is configured.\n // setInterval alone counts from daemon start, so a machine rebooted at noon\n // gets its \"daily\" maintenance at noon every day thereafter — the interval\n // says how often, never when. Without an anchor there is no such thing as a\n // night-only pass, however long the interval.\n const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);\n if (daemonConfig.maintenanceHour !== undefined) {\n process.stderr.write(\n `[pai-daemon] Index scheduler anchored to ${String(daemonConfig.maintenanceHour).padStart(2, \"0\")}:00 local ` +\n `(next pass in ${Math.round(firstDelayMs / 60_000)} min)\\n`\n );\n }\n\n let timer: ReturnType<typeof setInterval>;\n const startTimer = () => {\n timer = setInterval(() => void cycle(\"Scheduled\", true), intervalMs);\n if (timer.unref) timer.unref();\n setIndexSchedulerTimer(timer);\n };\n\n const first = setTimeout(() => {\n void cycle(\"Scheduled\", true);\n startTimer();\n }, firstDelayMs);\n if (first.unref) first.unref();\n}\n\n/**\n * Milliseconds until the next occurrence of `hour`:00 local time.\n * Falls back to the plain interval when no anchor hour is configured.\n */\nexport function msUntilNextAnchor(hour: number | undefined, intervalMs: number): number {\n if (hour === undefined || !Number.isInteger(hour) || hour < 0 || hour > 23) {\n return intervalMs;\n }\n const now = new Date();\n const next = new Date(now);\n next.setHours(hour, 0, 0, 0);\n if (next <= now) next.setDate(next.getDate() + 1);\n return next.getTime() - now.getTime();\n}\n\n// ---------------------------------------------------------------------------\n// Embed scheduler\n// ---------------------------------------------------------------------------\n\n/**\n * Run an embedding pass for all unembedded chunks (Postgres backend only).\n */\nexport async function runEmbed(): Promise<void> {\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index in progress, deferring embed pass.\\n\");\n return;\n }\n\n if (!storageBackend.supportsPostgresFeatures) {\n return;\n }\n\n setEmbedInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled embed pass...\\n\");\n\n const { count } = await runPassPrioritised(\"embed\", async () => {\n const projectNames = new Map<number, string>();\n try {\n const rows = await registryBackend.listProjects({ status: \"active\" });\n for (const r of rows) projectNames.set(r.id, r.slug);\n } catch { /* registry unavailable — IDs will be used instead */ }\n\n // Explicit budgets so one embed phase can never outlast an index cycle.\n // Backlogs drain across passes, not within one.\n const count = await embedChunksWithBackend(\n storageBackend,\n () => shutdownRequested,\n projectNames,\n // Indexing settles to a few chunks per pass once caught up, so the daemon\n // is idle most of the cycle and the embed budget is what actually sets\n // drain rate. Measured 450 chunks per 90s against real (long) chunks, so\n // a backlog only clears at roughly the budget's share of the cycle. What\n // broke the starvation was bounding the pass at all, not this number —\n // it can be raised freely as long as it stays finite.\n { maxMillis: 240_000 },\n );\n return { count };\n });\n\n // Vault chunks (project 999) are indexed through the same storageBackend\n // as everything else (indexVault() writes via backend.insertChunks()), so\n // the embedChunksWithBackend() call above already embeds them. A vault-only\n // SQLite pass here would open a stray federation database even under Postgres.\n\n const elapsed = Date.now() - t0;\n setLastEmbedTime(Date.now());\n passFailures.delete(\"embed\");\n process.stderr.write(\n `[pai-daemon] Embed pass complete: ${count} chunks embedded (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Embed error: ${msg}\\n`);\n failPass(\"embed\", e, runEmbed);\n } finally {\n setEmbedInProgress(false);\n }\n}\n\n/**\n * Start the periodic embed scheduler. Initial run is 60 seconds after startup.\n */\nexport function startEmbedScheduler(): void {\n const intervalMs = daemonConfig.embedIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\\n`\n );\n\n // Startup embed pass — OFF unless explicitly enabled.\n //\n // This used to run unconditionally 60s after every start, ignoring\n // embedIntervalSecs entirely. With a large backlog that makes every restart a\n // CPU storm: the interval was set to a day to stop the machine being bogged\n // down, the daemon was restarted, and it immediately began a 240-second pass\n // anyway. Throttling the recurring timer while leaving this armed means the\n // throttle silently does nothing for anyone who restarts.\n //\n // Measured backlog when this was found: 1,868,098 unembedded chunks at ~5.7\n // chunks/s — about 91 days of work that a restart would resume unbidden.\n if (daemonConfig.embedOnStartup) {\n setTimeout(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Startup embed error: ${e}\\n`);\n });\n }, 60_000);\n } else {\n process.stderr.write(\n \"[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\\n\"\n );\n }\n\n // With a maintenance hour configured the index cycle already chains an embed\n // pass onto every anchored run, so this timer would only add a second pass\n // that counts from daemon start — observed as \"every 86400s\" armed at a\n // 20:08 boot, i.e. a daily daytime pass on a machine configured for 03:00.\n // The interval says how often, never when; without an anchor it cannot\n // deliver a night-only pass, so it is not armed at all.\n if (daemonConfig.maintenanceHour !== undefined) {\n process.stderr.write(\n \"[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; \" +\n \"embed runs at the end of each anchored index cycle).\\n\"\n );\n return;\n }\n\n const timer = setInterval(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setEmbedSchedulerTimer(timer);\n}\n\n// ---------------------------------------------------------------------------\n// Registry scan scheduler\n// ---------------------------------------------------------------------------\n\nconst REGISTRY_SCAN_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes\nconst REGISTRY_SCAN_STARTUP_DELAY_MS = 10_000; // 10 seconds after daemon start\n\n/**\n * Start the periodic registry scan scheduler.\n * First scan runs 10 seconds after startup (enough time for IPC to stabilise).\n * Subsequent scans run every 30 minutes.\n * Uses the work-queue so the scan runs on the worker thread without blocking IPC.\n */\nexport function startRegistryScanScheduler(): void {\n process.stderr.write(\n \"[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\\n\"\n );\n\n setTimeout(() => {\n Promise.resolve()\n .then(() => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_STARTUP_DELAY_MS);\n\n const timer = setInterval(() => {\n Promise.resolve()\n .then(() => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_INTERVAL_MS);\n\n if (timer.unref) timer.unref();\n}\n\n// ---------------------------------------------------------------------------\n// Worker supervision scheduler\n// ---------------------------------------------------------------------------\n\n/** How often the daemon looks at the worker ledger for supervision events. */\nconst SUPERVISION_TICK_MS = 30_000;\n\n/** First supervision pass waits for the first statuses to exist at all. */\nconst SUPERVISION_STARTUP_DELAY_MS = 15_000;\n\n/**\n * Start the worker supervisor: one tick every 30 s over the workers ledger\n * (src/workers/supervision.ts) that pushes finished/failed/stalled events to\n * the owning session — the daemon-side replacement for orchestrator polling.\n * Daemon code only: no model call has any business being here, and none is\n * made. Disabled workers disable the supervisor with it.\n */\nexport function startWorkerSupervisor(): void {\n let enabled = false;\n try {\n enabled = readWorkersSection().workers.enabled;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Worker supervisor: not started (${e instanceof Error ? e.message : String(e)})\\n`\n );\n return;\n }\n if (!enabled) {\n process.stderr.write(\"[pai-daemon] Worker supervisor: disabled (workers are off)\\n\");\n return;\n }\n\n const tick = () => {\n runSupervisionTick(workersLogDir(readWorkersSection().workers), {\n stallMs: stallMinutesFromEnv() * 60_000,\n thrashFails: thrashFailsFromEnv(),\n })\n .then((r) => {\n for (const ev of r.events) {\n process.stderr.write(`[pai-daemon] Supervision: ${ev.text}\\n`);\n }\n })\n .catch((e) => {\n // a broken tick must never take the daemon down with it\n process.stderr.write(\n `[pai-daemon] Supervision tick error: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n });\n };\n\n const first = setTimeout(tick, SUPERVISION_STARTUP_DELAY_MS);\n if (first.unref) first.unref();\n const timer = setInterval(tick, SUPERVISION_TICK_MS);\n if (timer.unref) timer.unref();\n process.stderr.write(\n `[pai-daemon] Worker supervisor: every ${Math.round(SUPERVISION_TICK_MS / 1000)}s\\n`\n );\n}\n\n// ---------------------------------------------------------------------------\n// Worker prompt-cache keepalive scheduler\n// ---------------------------------------------------------------------------\n\n/** First beat waits a while after boot; the daemon start is itself a warm request. */\nconst KEEPALIVE_STARTUP_DELAY_MS = 60_000;\n\n/**\n * Start the cache keepalive: every workers.cacheKeepaliveSecs one trivial\n * single-turn worker (src/workers/keepalive.ts) re-arms the provider's\n * implicit prompt cache so real worker spawns start warm. Needs no storage\n * backend — it starts with the IPC server, not after the federation connect.\n * 0 (or workers off) keeps it off. A failed beat is logged and swallowed.\n */\nexport function startCacheKeepalive(opts: {\n configPath?: string;\n beat?: () => Promise<BeatMetrics | null>;\n} = {}): void {\n let workers: ReturnType<typeof readWorkersSection>[\"workers\"];\n try {\n workers = readWorkersSection(opts.configPath).workers;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Cache keepalive: not started (${e instanceof Error ? e.message : String(e)})\\n`\n );\n return;\n }\n if (!workers.enabled) {\n process.stderr.write(\"[pai-daemon] Cache keepalive: disabled (workers are off)\\n\");\n return;\n }\n const secs = keepaliveSecs(workers);\n if (!secs) {\n process.stderr.write(\"[pai-daemon] Cache keepalive: disabled (cacheKeepaliveSecs is 0)\\n\");\n return;\n }\n\n let notedNoPostgres = false;\n const supportsObservations = () => {\n if (storageBackend?.supportsPostgresFeatures) return true;\n if (!notedNoPostgres) {\n notedNoPostgres = true;\n process.stderr.write(\n \"[pai-daemon] Cache keepalive: observation rows need Postgres — ledger line only\\n\"\n );\n }\n return false;\n };\n\n const beat = async (): Promise<void> => {\n const m = await (opts.beat ?? runKeepaliveBeat)();\n if (!m) return; // overlap guard: previous beat still running\n if (!supportsObservations()) return; // ledger line already written by the beat itself\n try {\n await storageBackend.storeObservationWithProject(registryBackend, {\n session_id: m.id,\n type: \"change\",\n title: `cache keepalive ${m.ok ? \"ok\" : \"failed\"} (cache_read=${m.cache_read_input_tokens}, api_ms=${m.duration_api_ms})`,\n tool_name: \"cache-keepalive\",\n narrative: `beat ${m.id}: input=${m.input_tokens} cache_read=${m.cache_read_input_tokens} cache_creation=${m.cache_creation_input_tokens} duration_api_ms=${m.duration_api_ms}`,\n tool_input_summary: undefined,\n files_read: [],\n files_modified: [],\n concepts: [],\n });\n } catch (e) {\n // a failed observation must never fail the beat\n process.stderr.write(\n `[pai-daemon] Cache keepalive observation error: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n }\n };\n\n const first = setTimeout(() => {\n beat().catch((e) => {\n process.stderr.write(\n `[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n });\n }, KEEPALIVE_STARTUP_DELAY_MS);\n if (first.unref) first.unref();\n\n const timer = setInterval(() => {\n beat().catch((e) => {\n process.stderr.write(\n `[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n });\n }, secs * 1000);\n if (timer.unref) timer.unref();\n setCacheKeepaliveTimer(timer);\n process.stderr.write(`[pai-daemon] Cache keepalive: every ${secs}s\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Interactive-session prompt-cache keepalive scheduler\n// ---------------------------------------------------------------------------\n\n/** Tick cadence: coarse enough to be cheap, fine enough that a session\n * crossing idleMinutes gets beaten within a minute of becoming eligible. */\nconst SESSION_KEEPALIVE_TICK_MS = 60_000;\n\n/**\n * Start the interactive-session cache keepalive: every 60s, beat every live\n * session that has gone idle past `sessions.cacheKeepalive.idleMinutes`\n * (src/daemon/session-keepalive.ts). Off unless\n * `sessions.cacheKeepalive.enabled` is true — no hot-reload, matching\n * startCacheKeepalive above: config changes need a daemon restart.\n */\nexport function startSessionKeepalive(): void {\n const config = daemonConfig.sessions.cacheKeepalive;\n if (!config.enabled) {\n process.stderr.write(\"[pai-daemon] Session keepalive: disabled (sessions.cacheKeepalive.enabled is false)\\n\");\n return;\n }\n\n const tick = () => {\n runSessionKeepaliveTick(config).catch((e) => {\n process.stderr.write(\n `[pai-daemon] Session keepalive tick error: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n });\n };\n\n const timer = setInterval(tick, SESSION_KEEPALIVE_TICK_MS);\n if (timer.unref) timer.unref();\n process.stderr.write(\n `[pai-daemon] Session keepalive: every ${SESSION_KEEPALIVE_TICK_MS / 1000}s ` +\n `(idle >= ${config.idleMinutes}min, hours ${config.activeHours}, max ${config.maxBeats} beats/stretch)\\n`\n );\n}\n","/**\n * Shared types and project-row helpers used across all MCP tool handler modules.\n */\n\nimport { resolve } from \"node:path\";\nimport type { RegistryBackend, Project } from \"../../storage/registry-interface.js\";\n\nexport interface ToolContent {\n type: \"text\";\n text: string;\n}\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Shared row type — mirrors the projects schema (RegistryBackend's Project)\n// ---------------------------------------------------------------------------\n\nexport type ProjectRow = Project;\n\n// ---------------------------------------------------------------------------\n// Helper: lookup project_id by slug (also checks aliases)\n// ---------------------------------------------------------------------------\n\nexport async function lookupProjectId(\n registry: RegistryBackend,\n slug: string\n): Promise<number | null> {\n const bySlug = await registry.getProjectBySlug(slug);\n if (bySlug) return bySlug.id;\n\n const projectId = await registry.resolveAlias(slug);\n return projectId ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: detect project from a filesystem path\n// ---------------------------------------------------------------------------\n\nexport async function detectProjectFromPath(\n registry: RegistryBackend,\n fsPath: string\n): Promise<ProjectRow | null> {\n const resolved = resolve(fsPath);\n\n const exact = await registry.getProjectByRootPath(resolved);\n if (exact) return exact;\n\n const all = await registry.listProjectsByPathLengthDesc();\n\n for (const project of all) {\n if (\n resolved.startsWith(project.root_path + \"/\") ||\n resolved === project.root_path\n ) {\n return project;\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format project row for tool output\n// ---------------------------------------------------------------------------\n\nexport async function formatProject(registry: RegistryBackend, project: ProjectRow): Promise<string> {\n const sessionCount = await registry.countSessionsForProject(project.id);\n const lastSessionDate = await registry.getMostRecentSessionDate(project.id);\n const tags = await registry.listTagsForProject(project.id);\n const aliases = await registry.listAliasesForProject(project.id);\n\n const lines: string[] = [\n `slug: ${project.slug}`,\n `display_name: ${project.display_name}`,\n `root_path: ${project.root_path}`,\n `type: ${project.type}`,\n `status: ${project.status}`,\n `sessions: ${sessionCount}`,\n ];\n\n if (lastSessionDate) lines.push(`last_session: ${lastSessionDate}`);\n if (tags.length) lines.push(`tags: ${tags.join(\", \")}`);\n if (aliases.length) lines.push(`aliases: ${aliases.join(\", \")}`);\n if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);\n if (project.archived_at) {\n lines.push(\n `archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`\n );\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Rank search results by how the corpus links to them, not only by similarity.\n *\n * Why this exists: the store holds 33,709 wikilinks that are *facts* — one note\n * pointing at another, written by a person — alongside 2.4M chunks whose only\n * ranking signal is embedding similarity. Similarity answers \"what reads like\n * the query\". It cannot answer \"which of these is the one the others refer\n * back to\", which is usually the note worth reading first.\n *\n * The boost is deliberately query-local: it counts links *between the results\n * themselves*, not global popularity. A note linked by many other notes that\n * also match the query is a hub for that question. A note linked by half the\n * vault is merely popular, which is not the same thing and would flatten every\n * ranking toward the same few index pages.\n *\n * Links cost nothing to maintain — no embedding pass, no model call — so this\n * signal stays correct while the embedding backlog drains, and works for chunks\n * that have no embedding at all.\n */\n\nimport type { SearchResult } from \"./search.js\";\n\n/** A directed link between two note paths, as stored in vault_links. */\nexport interface LinkEdge {\n sourcePath: string;\n targetPath: string;\n}\n\nexport interface LinkBoostOptions {\n /**\n * How much the boost may move a result, as a fraction of its current score.\n * 0.25 means the most-linked result gains 25%. Kept modest by default: the\n * link graph is a supporting signal, and a note nobody links to can still be\n * the right answer.\n */\n weight?: number;\n}\n\n/**\n * Re-rank results by inbound links *from other results in the same set*.\n *\n * Returns a new array, sorted by the adjusted score. Input is not mutated.\n * Results whose paths carry no inbound links are unchanged, so a corpus with\n * no links at all is a no-op rather than a distortion.\n */\nexport function applyLinkBoost(\n results: SearchResult[],\n edges: LinkEdge[],\n opts?: LinkBoostOptions,\n): SearchResult[] {\n const weight = opts?.weight ?? 0.25;\n if (results.length === 0 || edges.length === 0 || weight === 0) {\n return [...results];\n }\n\n // Only links whose BOTH ends are in the result set count. An edge pointing\n // out of the set says nothing about the relative rank of results inside it.\n const paths = new Set(results.map((r) => r.path));\n const inbound = new Map<string, number>();\n for (const e of edges) {\n if (e.sourcePath === e.targetPath) continue; // self-links are noise\n if (!paths.has(e.sourcePath) || !paths.has(e.targetPath)) continue;\n inbound.set(e.targetPath, (inbound.get(e.targetPath) ?? 0) + 1);\n }\n if (inbound.size === 0) return [...results];\n\n // Normalise against the most-linked result so the boost is bounded by\n // `weight` regardless of corpus size. Without this, a densely linked project\n // would swamp similarity entirely while a sparse one would see no effect.\n const maxInbound = Math.max(...inbound.values());\n\n return results\n .map((r) => {\n const links = inbound.get(r.path) ?? 0;\n if (links === 0) return { ...r };\n const factor = 1 + weight * (links / maxInbound);\n return { ...r, score: r.score * factor };\n })\n .sort((a, b) => b.score - a.score);\n}\n","/**\n * MCP tool handlers: memory_search, memory_get, memory_outline\n */\n\nimport { readFileSync, existsSync, statSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport { populateSlugs } from \"../../memory/search.js\";\nimport { buildOutline, type OutlineNode } from \"../../memory/chunker.js\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport type { SearchConfig } from \"../../daemon/config.js\";\nimport type { SearchResult } from \"../../memory/search.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" in a long-running daemon (prune-dist.mjs deletes superseded chunks).\nimport { embedQuery } from \"../../memory/embedding-gate.js\";\nimport { applyLinkBoost } from \"../../memory/link-boost.js\";\nimport { rerankResults } from \"../../memory/reranker.js\";\nimport { applyRecencyBoost } from \"../../memory/search.js\";\nimport { saveQueryResult } from \"../../zettelkasten/query-feedback.js\";\nimport {\n lookupProjectId,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_search\n// ---------------------------------------------------------------------------\n\nexport interface MemorySearchParams {\n query: string;\n project?: string;\n all_projects?: boolean;\n sources?: Array<\"memory\" | \"notes\">;\n limit?: number;\n mode?: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Rerank results using cross-encoder model for better relevance ordering. */\n rerank?: boolean;\n /** Apply recency boost — score decays by half every N days. 0 = off (default). */\n recencyBoost?: number;\n /** Maximum characters per result snippet. Default 200.\n * Limit context consumption — MCP results go into Claude's context window. */\n snippetLength?: number;\n /** Output format: \"full\" (default, includes snippets) or \"compact\" (IDs + metadata only, ~10x fewer tokens). */\n format?: \"full\" | \"compact\";\n}\n\nexport async function toolMemorySearch(\n registry: RegistryBackend,\n federation: StorageBackend,\n params: MemorySearchParams,\n searchDefaults?: SearchConfig,\n): Promise<ToolResult> {\n try {\n const projectIds: number[] | undefined = params.project\n ? await (async () => {\n const id = await lookupProjectId(registry, params.project!);\n return id != null ? [id] : [];\n })()\n : undefined;\n\n if (params.project && (!projectIds || projectIds.length === 0)) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n // NOTE: No indexAll() here — indexing is handled by the daemon scheduler.\n // The daemon ensures the index stays fresh; the search hot path is read-only.\n\n const mode = params.mode ?? (searchDefaults?.mode ?? \"keyword\");\n // Limit context consumption — MCP results go into Claude's context window.\n // Default to 5 results and 200-char snippets to keep a single search call\n // within ~1-2K tokens rather than 5K+.\n const snippetLength = params.snippetLength ?? (searchDefaults?.snippetLength ?? 200);\n const searchOpts = {\n projectIds,\n sources: params.sources,\n maxResults: params.limit ?? (searchDefaults?.defaultLimit ?? 5),\n };\n\n let results: SearchResult[];\n let embedNote = \"\";\n\n if (mode === \"keyword\") {\n results = await federation.searchKeyword(params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const q = await embedQuery(federation, params.query);\n const queryEmbedding = q.vec;\n if (!queryEmbedding) {\n // Mismatched or unavailable backend: keyword-only, never another backend.\n embedNote = `\\nNote: ${q.note}`;\n results = await federation.searchKeyword(params.query, searchOpts);\n } else if (mode === \"semantic\") {\n results = await federation.searchSemantic(queryEmbedding, searchOpts);\n } else {\n // Hybrid: combine keyword + semantic\n const [kwResults, semResults] = await Promise.all([\n federation.searchKeyword(params.query, { ...searchOpts, maxResults: 50 }),\n federation.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 50 }),\n ]); // 50 candidates is sufficient for min-max normalization\n // Reuse the existing hybrid scoring logic\n results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);\n }\n } else {\n results = await federation.searchKeyword(params.query, searchOpts);\n }\n\n // Re-rank by the corpus's own links before returning.\n //\n // Similarity answers \"what reads like the query\"; it cannot answer \"which\n // of these is the one the others refer back to\", which is usually the note\n // to read first. The wikilinks are facts a person wrote, they cost nothing\n // to maintain, and they are correct for chunks that have no embedding yet —\n // which matters while a large embedding backlog drains.\n //\n // Only EXTRACTED links count: INFERRED ones are themselves derived by\n // similarity, so folding them in would feed the ranking its own output.\n try {\n const paths = [...new Set(results.map((r) => r.path))];\n // Bounded: one lookup per distinct result path, not per chunk, and the\n // result set is already capped by maxResults.\n if (paths.length > 1) {\n const edges: Array<{ sourcePath: string; targetPath: string }> = [];\n for (const p of paths) {\n for (const l of await federation.getLinksToTarget(p)) {\n if (l.confidence && l.confidence !== \"EXTRACTED\") continue;\n edges.push({ sourcePath: l.sourcePath, targetPath: p });\n }\n }\n if (edges.length > 0) results = applyLinkBoost(results, edges);\n }\n } catch {\n // A ranking aid, never a reason to fail a search.\n }\n\n // QW2: Update last_accessed_at for returned chunks (best-effort, non-blocking)\n try {\n const chunkIds = results\n .map((r) => r.chunkId)\n .filter((id): id is string => id != null);\n if (chunkIds.length > 0) {\n await federation.touchChunksLastAccessed(chunkIds);\n }\n } catch {\n // non-critical — never block search results\n }\n\n // Cross-encoder reranking (on by default)\n const shouldRerank = params.rerank ?? (searchDefaults?.rerank ?? true);\n if (shouldRerank && results.length > 0) {\n results = await rerankResults(params.query, results, {\n topK: searchOpts.maxResults ?? 5,\n });\n }\n\n // Recency boost (off by default, applied after reranking)\n const recencyDays = params.recencyBoost ?? (searchDefaults?.recencyBoostDays ?? 0);\n if (recencyDays > 0 && results.length > 0) {\n results = applyRecencyBoost(results, recencyDays);\n }\n\n const withSlugs = await populateSlugs(results, registry);\n\n if (withSlugs.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No results found for query: \"${params.query}\" (mode: ${mode})${embedNote}`,\n },\n ],\n };\n }\n\n const rerankLabel = shouldRerank ? \" +rerank\" : \"\";\n const useCompact = params.format === \"compact\";\n const formatted = withSlugs\n .map((r, i) => {\n const slug = r.projectSlug ?? `project:${r.projectId}`;\n const idPart = r.chunkId ? ` id=${r.chunkId}` : \"\";\n if (useCompact) {\n // Compact format: chunk ID, path, score — pass id to memory_feedback to rate this result\n return `[${i + 1}]${idPart} ${slug} — ${r.path} L${r.startLine}-${r.endLine} score=${r.score.toFixed(3)}`;\n }\n const header = `[${i + 1}]${idPart} ${slug} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;\n // Truncate snippet to snippetLength — limit context consumption.\n // MCP results go into Claude's context window; keep each result tight.\n const raw = r.snippet.trim();\n const snippet = raw.length > snippetLength\n ? raw.slice(0, snippetLength) + \"...\"\n : raw;\n return `${header}\\n${snippet}`;\n })\n .join(useCompact ? \"\\n\" : \"\\n\\n---\\n\\n\");\n\n // Query feedback loop: save query + result metadata for future indexing\n try {\n saveQueryResult({\n query: params.query,\n timestamp: Date.now(),\n source: \"memory_search\",\n sourceSlugs: withSlugs.slice(0, 5).map((r) => r.path),\n answerPreview: withSlugs.slice(0, 3).map((r) => r.snippet.trim().slice(0, 150)).join(\" | \"),\n resultCount: withSlugs.length,\n });\n } catch {\n // Non-critical — don't fail the search if feedback logging errors\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Found ${withSlugs.length} result(s) for \"${params.query}\" (mode: ${mode}${rerankLabel}):${embedNote}\\n\\n${formatted}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Search error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_get\n// ---------------------------------------------------------------------------\n\nexport interface MemoryGetParams {\n project: string;\n path: string;\n from?: number;\n lines?: number;\n}\n\n/**\n * Resolve `project` + `path` to a readable file inside the project root.\n * Shared by memory_get and memory_outline so both apply the same path and\n * access checks. Returns a ToolResult on any failure.\n */\nasync function resolveProjectFile(\n registry: RegistryBackend,\n projectSlug: string,\n requestedPath: string,\n): Promise<{ fullPath: string } | ToolResult> {\n const fail = (text: string): ToolResult => ({\n content: [{ type: \"text\", text }],\n isError: true,\n });\n\n const projectId = await lookupProjectId(registry, projectSlug);\n if (projectId == null) return fail(`Project not found: ${projectSlug}`);\n\n const project = await registry.getProjectById(projectId);\n if (!project) return fail(`Project not found: ${projectSlug}`);\n\n if (requestedPath.includes(\"..\") || isAbsolute(requestedPath)) {\n return fail(\n `Invalid path: ${requestedPath} (must be a relative path within the project root, no ../ allowed)`,\n );\n }\n\n const fullPath = join(project.root_path, requestedPath);\n const resolvedFull = resolve(fullPath);\n const resolvedRoot = resolve(project.root_path);\n\n if (!resolvedFull.startsWith(resolvedRoot + \"/\") && resolvedFull !== resolvedRoot) {\n return fail(`Path traversal blocked: ${requestedPath}`);\n }\n\n if (!existsSync(fullPath)) {\n return fail(`File not found: ${requestedPath} (project: ${projectSlug})`);\n }\n\n const stat = statSync(fullPath);\n if (stat.size > 5 * 1024 * 1024) {\n return {\n content: [\n {\n type: \"text\",\n text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`,\n },\n ],\n };\n }\n\n return { fullPath };\n}\n\nexport async function toolMemoryGet(\n registry: RegistryBackend,\n params: MemoryGetParams\n): Promise<ToolResult> {\n try {\n const resolved = await resolveProjectFile(registry, params.project, params.path);\n if (\"content\" in resolved) return resolved;\n const { fullPath } = resolved;\n const requestedPath = params.path;\n\n const content = readFileSync(fullPath, \"utf8\");\n const allLines = content.split(\"\\n\");\n\n const fromLine = (params.from ?? 1) - 1;\n const toLine =\n params.lines != null\n ? Math.min(fromLine + params.lines, allLines.length)\n : allLines.length;\n\n const selectedLines = allLines.slice(fromLine, toLine);\n const text = selectedLines.join(\"\\n\");\n\n const header =\n params.from != null\n ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):`\n : `${params.project}/${requestedPath}:`;\n\n return {\n content: [{ type: \"text\", text: `${header}\\n\\n${text}` }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_outline\n// ---------------------------------------------------------------------------\n\nexport interface MemoryOutlineParams {\n project: string;\n path: string;\n max_depth?: number;\n}\n\nfunction renderOutline(nodes: OutlineNode[], maxDepth: number, depth = 0, out: string[] = []): string[] {\n for (const n of nodes) {\n out.push(\n `${\" \".repeat(depth)}${\"#\".repeat(n.level)} ${n.title} L${n.startLine}-${n.endLine} ~${n.tokens}t`,\n );\n if (depth + 1 < maxDepth) renderOutline(n.children, maxDepth, depth + 1, out);\n }\n return out;\n}\n\nexport async function toolMemoryOutline(\n registry: RegistryBackend,\n params: MemoryOutlineParams\n): Promise<ToolResult> {\n try {\n const resolved = await resolveProjectFile(registry, params.project, params.path);\n if (\"content\" in resolved) return resolved;\n\n const outline = buildOutline(readFileSync(resolved.fullPath, \"utf8\"));\n const body = outline.length\n ? renderOutline(outline, params.max_depth ?? Infinity).join(\"\\n\")\n : \"(no headings)\";\n\n return {\n content: [{ type: \"text\", text: `${params.project}/${params.path} outline:\\n\\n${body}` }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search helper (backend-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Combine keyword + semantic results using min-max normalized scoring.\n * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,\n * but works on pre-computed result arrays so it works for any backend.\n */\nexport function combineHybridResults(\n keywordResults: SearchResult[],\n semanticResults: SearchResult[],\n maxResults: number,\n keywordWeight = 0.5,\n semanticWeight = 0.5\n): SearchResult[] {\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n","/**\n * MCP tool handlers: project_info, project_list, project_detect,\n * project_health, project_todo\n */\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { detectProject, formatDetectionJson } from \"../../cli/commands/detect.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\n// Static on purpose: a lazy dist import breaks a long-running daemon after a rebuild.\nimport { encodeDir } from \"../../cli/utils.js\";\nimport {\n lookupProjectId,\n detectProjectFromPath,\n formatProject,\n type ProjectRow,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: project_info\n// ---------------------------------------------------------------------------\n\nexport interface ProjectInfoParams {\n slug?: string;\n}\n\nexport async function toolProjectInfo(\n registry: RegistryBackend,\n params: ProjectInfoParams\n): Promise<ToolResult> {\n try {\n let project: ProjectRow | null = null;\n\n if (params.slug) {\n const projectId = await lookupProjectId(registry, params.slug);\n if (projectId != null) {\n project = await registry.getProjectById(projectId);\n }\n } else {\n const cwd = process.cwd();\n project = await detectProjectFromPath(registry, cwd);\n }\n\n if (!project) {\n const message = params.slug\n ? `Project not found: ${params.slug}`\n : `No PAI project found matching the current directory: ${process.cwd()}`;\n return {\n content: [{ type: \"text\", text: message }],\n isError: !params.slug,\n };\n }\n\n return {\n content: [{ type: \"text\", text: await formatProject(registry, project) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_info error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_list\n// ---------------------------------------------------------------------------\n\nexport interface ProjectListParams {\n status?: \"active\" | \"archived\" | \"migrating\";\n tag?: string;\n limit?: number;\n}\n\nexport async function toolProjectList(\n registry: RegistryBackend,\n params: ProjectListParams\n): Promise<ToolResult> {\n try {\n const limit = params.limit ?? 50;\n\n const projects = await registry.listProjects({\n status: params.status,\n tagName: params.tag,\n orderBy: \"updated_desc\",\n limit,\n });\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No projects found matching the given filters.\",\n },\n ],\n };\n }\n\n const lines = projects.map(\n (p) =>\n `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} project(s):\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_detect\n// ---------------------------------------------------------------------------\n\nexport interface ProjectDetectParams {\n cwd?: string;\n}\n\nexport async function toolProjectDetect(\n params: ProjectDetectParams\n): Promise<ToolResult> {\n try {\n const detection = await detectProject(params.cwd);\n\n if (!detection) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: `No registered project found for path: ${target}\\n\\nRun 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatDetectionJson(detection) }],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_detect error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_health\n// ---------------------------------------------------------------------------\n\nexport interface ProjectHealthParams {\n category?: \"active\" | \"stale\" | \"dead\" | \"all\";\n}\n\nexport async function toolProjectHealth(\n registry: RegistryBackend,\n params: ProjectHealthParams\n): Promise<ToolResult> {\n try {\n const { existsSync: fsExists, readdirSync, statSync } = await import(\n \"node:fs\"\n );\n const {\n join: pathJoin,\n basename: pathBasename,\n } = await import(\"node:path\");\n const { homedir } = await import(\"node:os\");\n const enc = encodeDir;\n\n const rows = (await registry.listProjectsWithSessionStats())\n .slice()\n .sort((a, b) => a.slug.localeCompare(b.slug));\n\n const home = homedir();\n const claudeProjects = pathJoin(home, \".claude\", \"projects\");\n\n function suggestMoved(rootPath: string): string | undefined {\n const name = pathBasename(rootPath);\n const candidates = [\n pathJoin(home, \"dev\", name),\n pathJoin(home, \"dev\", \"ai\", name),\n pathJoin(home, \"Desktop\", name),\n pathJoin(home, \"Projects\", name),\n ];\n return candidates.find((c) => fsExists(c));\n }\n\n function hasClaudeNotes(encodedDir: string): boolean {\n if (!fsExists(claudeProjects)) return false;\n try {\n for (const entry of readdirSync(claudeProjects)) {\n if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;\n const full = pathJoin(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n if (fsExists(pathJoin(full, \"Notes\"))) return true;\n }\n } catch {\n /* ignore */\n }\n return false;\n }\n\n interface HealthResult {\n slug: string;\n display_name: string;\n root_path: string;\n status: string;\n type: string;\n session_count: number;\n health: string;\n suggested_path: string | null;\n has_claude_notes: boolean;\n todo: {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n };\n }\n\n function findTodoForProject(rootPath: string): {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n } {\n const locs = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n ];\n for (const rel of locs) {\n const full = pathJoin(rootPath, rel);\n if (fsExists(full)) {\n try {\n const raw = readFileSync(full, \"utf8\");\n const hasContinue = /^## Continue$/m.test(raw);\n return { found: true, path: rel, has_continue: hasContinue };\n } catch {\n return { found: true, path: rel, has_continue: false };\n }\n }\n }\n return { found: false, path: null, has_continue: false };\n }\n\n const results: HealthResult[] = rows.map((p) => {\n const pathExists = fsExists(p.root_path);\n let health: string;\n let suggestedPath: string | null = null;\n\n if (pathExists) {\n health = \"active\";\n } else {\n suggestedPath = suggestMoved(p.root_path) ?? null;\n health = suggestedPath ? \"stale\" : \"dead\";\n }\n\n const todo = pathExists\n ? findTodoForProject(p.root_path)\n : { found: false, path: null, has_continue: false };\n\n return {\n slug: p.slug,\n display_name: p.display_name,\n root_path: p.root_path,\n status: p.status,\n type: p.type,\n session_count: p.session_count,\n health,\n suggested_path: suggestedPath,\n has_claude_notes: hasClaudeNotes(p.encoded_dir),\n todo,\n };\n });\n\n const filtered =\n !params.category || params.category === \"all\"\n ? results\n : results.filter((r) => r.health === params.category);\n\n const summary = {\n total: rows.length,\n active: results.filter((r) => r.health === \"active\").length,\n stale: results.filter((r) => r.health === \"stale\").length,\n dead: results.filter((r) => r.health === \"dead\").length,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ summary, projects: filtered }, null, 2),\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_health error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_todo\n// ---------------------------------------------------------------------------\n\nexport interface ProjectTodoParams {\n project?: string;\n}\n\n/**\n * TODO candidate locations searched in priority order.\n * Returns the first one that exists, along with its label.\n */\nconst TODO_LOCATIONS = [\n { rel: \"Notes/TODO.md\", label: \"Notes/TODO.md\" },\n { rel: \".claude/Notes/TODO.md\", label: \".claude/Notes/TODO.md\" },\n { rel: \"tasks/todo.md\", label: \"tasks/todo.md\" },\n { rel: \"TODO.md\", label: \"TODO.md\" },\n];\n\n/**\n * Given TODO file content, extract and surface the ## Continue section first,\n * then return the remaining content. Returns an object with:\n * continueSection: string | null\n * fullContent: string\n * hasContinue: boolean\n */\nfunction parseTodoContent(raw: string): {\n continueSection: string | null;\n fullContent: string;\n hasContinue: boolean;\n} {\n const lines = raw.split(\"\\n\");\n\n // Find the ## Continue heading\n const continueIdx = lines.findIndex(\n (l) => l.trim() === \"## Continue\"\n );\n\n if (continueIdx === -1) {\n return { continueSection: null, fullContent: raw, hasContinue: false };\n }\n\n // The section ends at the first `---` separator or next `##` heading after\n // the Continue heading (whichever comes first).\n let endIdx = lines.length;\n for (let i = continueIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n endIdx = i;\n break;\n }\n }\n\n const continueLines = lines.slice(continueIdx, endIdx);\n const continueSection = continueLines.join(\"\\n\").trim();\n\n return { continueSection, fullContent: raw, hasContinue: true };\n}\n\nexport async function toolProjectTodo(\n registry: RegistryBackend,\n params: ProjectTodoParams\n): Promise<ToolResult> {\n try {\n let rootPath: string;\n let projectSlug: string;\n\n if (params.project) {\n const projectId = await lookupProjectId(registry, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const row = await registry.getProjectById(projectId);\n\n if (!row) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n rootPath = row.root_path;\n projectSlug = row.slug;\n } else {\n // Auto-detect from cwd\n const project = await detectProjectFromPath(registry, process.cwd());\n if (!project) {\n return {\n content: [\n {\n type: \"text\",\n text: `No PAI project found matching the current directory: ${process.cwd()}\\n\\nProvide a project slug or run 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n rootPath = project.root_path;\n projectSlug = project.slug;\n }\n\n // Search for TODO in priority order\n for (const loc of TODO_LOCATIONS) {\n const fullPath = join(rootPath, loc.rel);\n if (existsSync(fullPath)) {\n const raw = readFileSync(fullPath, \"utf8\");\n const { continueSection, fullContent, hasContinue } = parseTodoContent(raw);\n\n let output: string;\n if (hasContinue && continueSection) {\n // Surface the ## Continue section first, then the full content\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n \"=== CONTINUE SECTION (surfaced first) ===\",\n continueSection,\n \"\",\n \"=== FULL TODO CONTENT ===\",\n fullContent,\n ].join(\"\\n\");\n } else {\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n fullContent,\n ].join(\"\\n\");\n }\n\n return {\n content: [{ type: \"text\", text: output }],\n };\n }\n }\n\n // No TODO found in any location\n const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join(\"\\n\");\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No TODO.md found for project: ${projectSlug}`,\n \"\",\n \"Searched locations (in order):\",\n searched,\n \"\",\n \"Create a TODO with: echo '## Tasks\\\\n- [ ] First task' > Notes/TODO.md\",\n ].join(\"\\n\"),\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_todo error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers: session_list, session_route\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" in a long-running daemon (prune-dist.mjs deletes superseded chunks).\nimport { autoRoute, formatAutoRouteJson } from \"../../session/auto-route.js\";\nimport {\n lookupProjectId,\n type ToolResult,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: session_list\n// ---------------------------------------------------------------------------\n\nexport interface SessionListParams {\n project: string;\n limit?: number;\n status?: \"open\" | \"completed\" | \"compacted\";\n}\n\nexport async function toolSessionList(\n registry: RegistryBackend,\n params: SessionListParams\n): Promise<ToolResult> {\n try {\n const projectId = await lookupProjectId(registry, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const limit = params.limit ?? 10;\n const sessions = await registry.listSessions({\n projectId,\n status: params.status,\n limit,\n });\n\n if (sessions.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No sessions found for project: ${params.project}`,\n },\n ],\n };\n }\n\n const lines = sessions.map(\n (s) =>\n `#${String(s.number).padStart(4, \"0\")} ${s.date} [${s.status}] ${s.title}\\n file: Notes/${s.filename}`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${sessions.length} session(s) for ${params.project}:\\n\\n${lines.join(\"\\n\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_route\n// ---------------------------------------------------------------------------\n\nexport interface SessionRouteParams {\n /** Working directory to route from (defaults to process.cwd()) */\n cwd?: string;\n /** Optional conversation context for topic-based fallback routing */\n context?: string;\n}\n\n/**\n * Automatically suggest which project a session belongs to.\n *\n * Strategy (in priority order):\n * 1. path — exact or parent-directory match in the project registry\n * 2. marker — walk up from cwd looking for Notes/PAI.md\n * 3. topic — BM25 keyword search against memory (requires context)\n *\n * Call this at session start (e.g., from CLAUDE.md or a session-start hook)\n * to automatically route the session to the correct project.\n */\nexport async function toolSessionRoute(\n registry: RegistryBackend,\n federation: StorageBackend,\n params: SessionRouteParams\n): Promise<ToolResult> {\n try {\n\n const result = await autoRoute(\n registry,\n federation,\n params.cwd,\n params.context\n );\n\n if (!result) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No project match found for: ${target}`,\n \"\",\n \"Tried: path match, PAI.md marker walk\" +\n (params.context ? \", topic detection\" : \"\"),\n \"\",\n \"Run 'pai project add .' to register this directory,\",\n \"or provide conversation context for topic-based routing.\",\n ].join(\"\\n\"),\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatAutoRouteJson(result) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_route error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handler: registry_search\n */\n\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: registry_search\n// ---------------------------------------------------------------------------\n\nexport interface RegistrySearchParams {\n query: string;\n}\n\nexport async function toolRegistrySearch(\n registry: RegistryBackend,\n params: RegistrySearchParams\n): Promise<ToolResult> {\n try {\n const projects = await registry.searchProjects(params.query, 20);\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No projects found matching: \"${params.query}\"`,\n },\n ],\n };\n }\n\n const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} match(es) for \"${params.query}\":\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `registry_search error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers: zettel_explore, zettel_health, zettel_surprise,\n * zettel_suggest, zettel_converse, zettel_themes,\n * zettel_god_notes, zettel_communities\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" in a long-running daemon (prune-dist.mjs deletes superseded chunks).\nimport { zettelExplore, zettelHealth, zettelSurprise, zettelSuggest, zettelConverse, zettelThemes, zettelGodNotes, zettelCommunities } from \"../../zettelkasten/index.js\";\nimport { saveQueryResult } from \"../../zettelkasten/query-feedback.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_explore\n// ---------------------------------------------------------------------------\n\nexport interface ZettelExploreParams {\n start_note: string;\n depth?: number;\n direction?: string;\n mode?: string;\n}\n\nexport async function toolZettelExplore(\n backend: StorageBackend,\n params: ZettelExploreParams\n): Promise<ToolResult> {\n try {\n const result = await zettelExplore(backend, {\n startNote: params.start_note,\n depth: params.depth,\n direction: params.direction as \"forward\" | \"backward\" | \"both\" | undefined,\n mode: params.mode as \"sequential\" | \"associative\" | \"all\" | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_explore error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_health\n// ---------------------------------------------------------------------------\n\nexport interface ZettelHealthParams {\n scope?: string;\n project_path?: string;\n recent_days?: number;\n include?: string[];\n}\n\nexport async function toolZettelHealth(\n backend: StorageBackend,\n params: ZettelHealthParams\n): Promise<ToolResult> {\n try {\n const result = await zettelHealth(backend, {\n scope: params.scope as \"full\" | \"recent\" | \"project\" | undefined,\n projectPath: params.project_path,\n recentDays: params.recent_days,\n include: params.include as Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\"> | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_health error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_surprise\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSurpriseParams {\n reference_path: string;\n vault_project_id: number;\n limit?: number;\n min_similarity?: number;\n min_graph_distance?: number;\n}\n\nexport async function toolZettelSurprise(\n backend: StorageBackend,\n params: ZettelSurpriseParams\n): Promise<ToolResult> {\n try {\n const results = await zettelSurprise(backend, {\n referencePath: params.reference_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n minSimilarity: params.min_similarity,\n minGraphDistance: params.min_graph_distance,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_surprise error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_suggest\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSuggestParams {\n note_path: string;\n vault_project_id: number;\n limit?: number;\n exclude_linked?: boolean;\n}\n\nexport async function toolZettelSuggest(\n backend: StorageBackend,\n params: ZettelSuggestParams\n): Promise<ToolResult> {\n try {\n const results = await zettelSuggest(backend, {\n notePath: params.note_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n excludeLinked: params.exclude_linked,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_suggest error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_converse\n// ---------------------------------------------------------------------------\n\nexport interface ZettelConverseParams {\n question: string;\n vault_project_id: number;\n depth?: number;\n limit?: number;\n}\n\nexport async function toolZettelConverse(\n backend: StorageBackend,\n params: ZettelConverseParams\n): Promise<ToolResult> {\n try {\n const result = await zettelConverse(backend, {\n question: params.question,\n vaultProjectId: params.vault_project_id,\n depth: params.depth,\n limit: params.limit,\n });\n\n // Query feedback loop: save query + result metadata for future indexing\n try {\n saveQueryResult({\n query: params.question,\n timestamp: Date.now(),\n source: \"zettel_converse\",\n sourceSlugs: result.relevantNotes.slice(0, 5).map((n) => n.path),\n answerPreview: result.relevantNotes.slice(0, 3).map((n) => {\n const title = n.title ?? \"(untitled)\";\n return `${title}: ${n.snippet.trim().slice(0, 100)}`;\n }).join(\" | \"),\n resultCount: result.relevantNotes.length,\n });\n } catch {\n // Non-critical\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_converse error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_themes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelThemesParams {\n vault_project_id: number;\n lookback_days?: number;\n min_cluster_size?: number;\n max_themes?: number;\n similarity_threshold?: number;\n}\n\nexport async function toolZettelThemes(\n backend: StorageBackend,\n params: ZettelThemesParams\n): Promise<ToolResult> {\n try {\n const result = await zettelThemes(backend, {\n vaultProjectId: params.vault_project_id,\n lookbackDays: params.lookback_days,\n minClusterSize: params.min_cluster_size,\n maxThemes: params.max_themes,\n similarityThreshold: params.similarity_threshold,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_themes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_god_notes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelGodNotesParams {\n limit?: number;\n min_inbound?: number;\n}\n\nexport async function toolZettelGodNotes(\n backend: StorageBackend,\n params: ZettelGodNotesParams\n): Promise<ToolResult> {\n try {\n const result = await zettelGodNotes(backend, {\n limit: params.limit,\n minInbound: params.min_inbound,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_god_notes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_communities\n// ---------------------------------------------------------------------------\n\nexport interface ZettelCommunitiesParams {\n min_size?: number;\n max_communities?: number;\n resolution?: number;\n}\n\nexport async function toolZettelCommunities(\n backend: StorageBackend,\n params: ZettelCommunitiesParams\n): Promise<ToolResult> {\n try {\n const result = await zettelCommunities(backend, {\n minSize: params.min_size,\n maxCommunities: params.max_communities,\n resolution: params.resolution,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_communities error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handlers for the temporal knowledge graph.\n *\n * Tools:\n * kg_add — insert a new (subject, predicate, object) triple\n * kg_query — query triples with optional temporal filter\n * kg_invalidate — mark a triple as no longer valid (sets valid_to)\n * kg_contradictions — find (subject, predicate) pairs with multiple valid objects\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: kg_add\n// ---------------------------------------------------------------------------\n\nexport interface KgAddToolParams {\n subject: string;\n predicate: string;\n object: string;\n project_id?: number;\n source_session?: string;\n confidence?: \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\";\n}\n\nexport async function toolKgAdd(\n backend: StorageBackend,\n params: KgAddToolParams\n): Promise<ToolResult> {\n try {\n if (!params.subject || !params.predicate || !params.object) {\n return {\n content: [{ type: \"text\", text: \"kg_add error: subject, predicate, and object are required\" }],\n isError: true,\n };\n }\n const triple = await backend.addKgTriple(params);\n return {\n content: [{ type: \"text\", text: JSON.stringify(triple, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_add error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_query\n// ---------------------------------------------------------------------------\n\nexport interface KgQueryToolParams {\n subject?: string;\n predicate?: string;\n object?: string;\n project_id?: number;\n as_of?: string; // ISO 8601 string; converted to Date\n include_invalidated?: boolean;\n}\n\nexport async function toolKgQuery(\n backend: StorageBackend,\n params: KgQueryToolParams\n): Promise<ToolResult> {\n try {\n const asOf = params.as_of ? new Date(params.as_of) : undefined;\n if (asOf && isNaN(asOf.getTime())) {\n return {\n content: [{ type: \"text\", text: `kg_query error: invalid as_of date: ${params.as_of}` }],\n isError: true,\n };\n }\n const triples = await backend.queryKgTriples({\n subject: params.subject,\n predicate: params.predicate,\n object: params.object,\n project_id: params.project_id,\n as_of: asOf,\n include_invalidated: params.include_invalidated,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(triples, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_query error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_invalidate\n// ---------------------------------------------------------------------------\n\nexport interface KgInvalidateToolParams {\n triple_id: number;\n}\n\nexport async function toolKgInvalidate(\n backend: StorageBackend,\n params: KgInvalidateToolParams\n): Promise<ToolResult> {\n try {\n if (params.triple_id === undefined || params.triple_id === null) {\n return {\n content: [{ type: \"text\", text: \"kg_invalidate error: triple_id is required\" }],\n isError: true,\n };\n }\n await backend.invalidateKgTriple(params.triple_id);\n return {\n content: [{ type: \"text\", text: JSON.stringify({ invalidated: true, triple_id: params.triple_id }) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_invalidate error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: kg_contradictions\n// ---------------------------------------------------------------------------\n\nexport interface KgContradictionsToolParams {\n subject: string;\n}\n\nexport async function toolKgContradictions(\n backend: StorageBackend,\n params: KgContradictionsToolParams\n): Promise<ToolResult> {\n try {\n if (!params.subject) {\n return {\n content: [{ type: \"text\", text: \"kg_contradictions error: subject is required\" }],\n isError: true,\n };\n }\n const contradictions = await backend.getKgContradictions(params.subject);\n return {\n content: [{ type: \"text\", text: JSON.stringify(contradictions, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `kg_contradictions error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handler: memory_wakeup\n *\n * Returns the L0+L1 wake-up context block for a project:\n * L0 — user identity from ~/.pai/identity.txt\n * L1 — recent session note highlights (Work Done / Key Decisions / Next Steps)\n */\n\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport { buildWakeupContext } from \"../../memory/wakeup.js\";\nimport { detectProjectFromPath } from \"./types.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Default token budget (constant, overridable per call)\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TOKEN_BUDGET = 800;\n\n// ---------------------------------------------------------------------------\n// Params\n// ---------------------------------------------------------------------------\n\nexport interface MemoryWakeupParams {\n /** Project slug or absolute path. Omit to auto-detect from process.cwd(). */\n project?: string;\n /** L1 token budget. Default 800. */\n token_budget?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\nexport async function toolMemoryWakeup(\n registry: RegistryBackend,\n params: MemoryWakeupParams\n): Promise<ToolResult> {\n try {\n const tokenBudget = params.token_budget ?? DEFAULT_TOKEN_BUDGET;\n\n // Resolve the project root path\n let rootPath: string | undefined;\n\n if (params.project) {\n // Try slug lookup first\n const bySlug = await registry.getProjectBySlug(params.project);\n\n if (bySlug) {\n rootPath = bySlug.root_path;\n } else {\n // Maybe it's an absolute path — try path-based detect\n const detected = await detectProjectFromPath(registry, params.project);\n if (detected) rootPath = detected.root_path;\n }\n } else {\n // Auto-detect from cwd\n const detected = await detectProjectFromPath(registry, process.cwd());\n if (detected) rootPath = detected.root_path;\n }\n\n const wakeupBlock = buildWakeupContext(rootPath, tokenBudget);\n\n if (!wakeupBlock) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No wake-up context available. Create ~/.pai/identity.txt for L0 identity, or ensure session notes exist for L1 story.\",\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `WAKEUP CONTEXT\\n\\n${wakeupBlock}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Wakeup context error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * getTaxonomy — returns the shape of stored memory without requiring a query.\n *\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n *\n * Uses the registry DB (projects, sessions) and the storage backend\n * (memory_files, memory_chunks) to build a structural overview.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Return types\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyProject {\n slug: string;\n display_name: string;\n session_count: number;\n note_count: number;\n last_activity: string | null; // ISO date string, e.g. \"2026-04-07\"\n top_tags: string[]; // project tags from the registry\n}\n\nexport interface TaxonomyTotals {\n projects: number;\n sessions: number;\n notes: number;\n chunks: number;\n}\n\nexport interface TaxonomyRecentActivity {\n project_slug: string;\n action: string;\n timestamp: string; // ISO date string\n}\n\nexport interface TaxonomyResult {\n projects: TaxonomyProject[];\n totals: TaxonomyTotals;\n recent_activity: TaxonomyRecentActivity[];\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyOptions {\n /** Include archived projects. Default: false. */\n include_archived?: boolean;\n /** Maximum projects to return. Default: 50. */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Main function\n// ---------------------------------------------------------------------------\n\n/**\n * Build a taxonomy of stored memory — what projects exist, how much is stored,\n * and what has been active recently.\n *\n * Registry queries (projects, sessions) go through RegistryBackend (async).\n * Storage backend queries (files, chunks) are async.\n */\nexport async function getTaxonomy(\n registry: RegistryBackend,\n storage: StorageBackend,\n options: TaxonomyOptions = {}\n): Promise<TaxonomyResult> {\n const includeArchived = options.include_archived ?? false;\n const limit = options.limit ?? 50;\n\n // -------------------------------------------------------------------------\n // 1. Load all (active) projects from the registry\n // -------------------------------------------------------------------------\n\n const projectRows = includeArchived\n ? await registry.listProjects({ orderBy: \"updated_desc\", limit })\n : await registry.listProjects({ status: \"active\", orderBy: \"updated_desc\", limit });\n\n if (projectRows.length === 0) {\n return {\n projects: [],\n totals: { projects: 0, sessions: 0, notes: 0, chunks: 0 },\n recent_activity: [],\n };\n }\n\n const projectIds = projectRows.map((p) => p.id);\n const projectIdSet = new Set(projectIds);\n\n // -------------------------------------------------------------------------\n // 2. Session counts per project (registry, async)\n // -------------------------------------------------------------------------\n\n const sessionCountsByProject = new Map<number, number>();\n const lastSessionDateByProject = new Map<number, string | null>();\n\n for (const projectId of projectIds) {\n sessionCountsByProject.set(projectId, await registry.countSessionsForProject(projectId));\n lastSessionDateByProject.set(projectId, await registry.getMostRecentSessionDate(projectId));\n }\n\n // -------------------------------------------------------------------------\n // 3. Tags per project (registry, async)\n // -------------------------------------------------------------------------\n\n const tagsByProject = new Map<number, string[]>();\n\n for (const projectId of projectIds) {\n tagsByProject.set(projectId, await registry.listTagsForProject(projectId));\n }\n\n // -------------------------------------------------------------------------\n // 4. Note and chunk counts per project (storage backend, async)\n // memory_files (one row per indexed file) and memory_chunks (many per\n // file), via StorageBackend.getProjectStats() — same on both backends.\n // -------------------------------------------------------------------------\n\n const noteCountsByProject = new Map<number, number>();\n const chunkCountsByProject = new Map<number, number>();\n\n for (const projectId of projectIds) {\n const projectStats = await storage.getProjectStats(projectId);\n noteCountsByProject.set(projectId, projectStats.files);\n chunkCountsByProject.set(projectId, projectStats.chunks);\n }\n\n // -------------------------------------------------------------------------\n // 5. Global totals\n // -------------------------------------------------------------------------\n\n const stats = await storage.getStats();\n\n const totalProjects = includeArchived\n ? await registry.countProjects()\n : await registry.countProjects({ status: \"active\" });\n\n // No RegistryBackend method for a global session count (flagged in the\n // report) — approximate as the sum over the projects already fetched\n // above (bounded by `limit`), the closest available without raw SQL.\n const totalSessions = [...sessionCountsByProject.values()].reduce((a, b) => a + b, 0);\n\n // -------------------------------------------------------------------------\n // 6. Recent activity — last 10 sessions across all (visible) projects\n // No RegistryBackend method filters sessions by their project's status\n // directly (flagged in the report) — over-fetch and filter client-side\n // against the project set already resolved above.\n // -------------------------------------------------------------------------\n\n const recentSessions = (await registry.listSessions({ limit: Math.max(50, limit) }))\n .filter((s) => projectIdSet.has(s.project_id))\n .slice(0, 10);\n\n const recentActivity: TaxonomyRecentActivity[] = recentSessions.map((row) => ({\n project_slug: row.project_slug,\n action: `session: ${row.title || \"(untitled)\"}`,\n timestamp: row.date,\n }));\n\n // -------------------------------------------------------------------------\n // 7. Assemble result\n // -------------------------------------------------------------------------\n\n const projects: TaxonomyProject[] = projectRows.map((row) => ({\n slug: row.slug,\n display_name: row.display_name,\n session_count: sessionCountsByProject.get(row.id) ?? 0,\n note_count: noteCountsByProject.get(row.id) ?? 0,\n last_activity: lastSessionDateByProject.get(row.id) ?? null,\n top_tags: tagsByProject.get(row.id) ?? [],\n }));\n\n return {\n projects,\n totals: {\n projects: totalProjects,\n sessions: totalSessions,\n notes: stats.files,\n chunks: stats.chunks,\n },\n recent_activity: recentActivity,\n };\n}\n","/**\n * MCP tool handler: memory_taxonomy\n *\n * Returns the SHAPE of stored memory without requiring a query.\n * Answers \"what do I know about?\" not \"what do I know about X?\"\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport { getTaxonomy } from \"../../memory/taxonomy.js\";\nimport type { ToolResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_taxonomy\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTaxonomyParams {\n /** Include archived projects in the result. Default: false. */\n include_archived?: boolean;\n /** Maximum number of projects to return. Default: 50. */\n limit?: number;\n}\n\nexport async function toolMemoryTaxonomy(\n registry: RegistryBackend,\n storage: StorageBackend,\n params: MemoryTaxonomyParams = {}\n): Promise<ToolResult> {\n try {\n const result = await getTaxonomy(registry, storage, {\n include_archived: params.include_archived,\n limit: params.limit,\n });\n\n // -----------------------------------------------------------------------\n // Format output as human-readable text\n // -----------------------------------------------------------------------\n\n const lines: string[] = [];\n\n // Totals header\n lines.push(\n `PAI Memory Taxonomy — ${result.totals.projects} project(s), ` +\n `${result.totals.sessions} session(s), ` +\n `${result.totals.notes} indexed file(s), ` +\n `${result.totals.chunks} chunk(s)`\n );\n lines.push(\"\");\n\n // Per-project breakdown\n if (result.projects.length === 0) {\n lines.push(\"No active projects found.\");\n } else {\n lines.push(\"Projects:\");\n for (const p of result.projects) {\n const tagStr = p.top_tags.length > 0 ? ` [${p.top_tags.join(\", \")}]` : \"\";\n const activityStr = p.last_activity ? ` last: ${p.last_activity}` : \"\";\n lines.push(\n ` ${p.slug} — ${p.display_name}` +\n ` sessions=${p.session_count}` +\n (p.note_count > 0 ? ` files=${p.note_count}` : \"\") +\n activityStr +\n tagStr\n );\n }\n }\n\n // Recent activity\n if (result.recent_activity.length > 0) {\n lines.push(\"\");\n lines.push(\"Recent activity (last 10 sessions across all projects):\");\n for (const a of result.recent_activity) {\n lines.push(` ${a.timestamp} ${a.project_slug} ${a.action}`);\n }\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_taxonomy error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * tunnels.ts — cross-project concept detection (\"palace graph / tunnel detection\")\n *\n * A \"tunnel\" is a concept (word or short phrase) that appears in chunks from\n * at least two distinct projects. These serendipitous cross-project connections\n * are surfaced so the user can discover unexpected relationships between their\n * work streams.\n *\n * The actual SQL (SQLite FTS5 vocab / Postgres ts_stat) lives under\n * src/storage/{sqlite,postgres}/tunnels.ts, reached through\n * StorageBackend.findTunnels() — this module just re-exports the public\n * types and forwards the call, so existing callers (MCP tools) don't have\n * to change their import path.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface Tunnel {\n /** The shared term or phrase. */\n concept: string;\n /** Project slugs where this concept appears. */\n projects: string[];\n /** Total chunk occurrences across all projects. */\n occurrences: number;\n /** First time the concept appeared (Unix ms). */\n first_seen: number;\n /** Most recent time the concept appeared (Unix ms). */\n last_seen: number;\n}\n\nexport interface FindTunnelsOptions {\n /** Minimum distinct projects a concept must appear in. Default 2. */\n min_projects?: number;\n /** Minimum total chunk occurrences across all projects. Default 3. */\n min_occurrences?: number;\n /** Maximum number of tunnels to return. Default 20. */\n limit?: number;\n}\n\nexport interface FindTunnelsResult {\n tunnels: Tunnel[];\n projects_analyzed: number;\n total_concepts_evaluated: number;\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Find cross-project concept tunnels.\n *\n * Works with both SQLite and Postgres storage backends.\n * Requires the RegistryBackend for project slug resolution.\n *\n * @param backend Active PAI storage backend.\n * @param registry Registry backend for project slug resolution.\n * @param options Filter and limit options.\n */\nexport async function findTunnels(\n backend: StorageBackend,\n registry: RegistryBackend,\n options?: FindTunnelsOptions\n): Promise<FindTunnelsResult> {\n return backend.findTunnels(registry, options);\n}\n","/**\n * MCP tool handler: memory_tunnels\n *\n * Finds \"tunnels\" — concepts that appear across multiple projects —\n * surfacing cross-project serendipitous connections in the PAI memory graph.\n */\n\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport type { ToolResult } from \"./types.js\";\nimport { findTunnels } from \"../../memory/tunnels.js\";\n\n// ---------------------------------------------------------------------------\n// Tool: memory_tunnels\n// ---------------------------------------------------------------------------\n\nexport interface MemoryTunnelsParams {\n /** Minimum distinct projects a concept must appear in. Default 2. */\n min_projects?: number;\n /** Minimum total chunk occurrences across all projects. Default 3. */\n min_occurrences?: number;\n /** Maximum number of tunnels to return. Default 20. */\n limit?: number;\n}\n\nexport async function toolMemoryTunnels(\n registry: RegistryBackend,\n backend: StorageBackend,\n params: MemoryTunnelsParams\n): Promise<ToolResult> {\n try {\n const result = await findTunnels(backend, registry, {\n min_projects: params.min_projects,\n min_occurrences: params.min_occurrences,\n limit: params.limit,\n });\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_tunnels error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * MCP tool handler: memory_feedback\n *\n * Implements the MR2 feedback weight loop:\n * - Accepts chunk IDs and a rating (1-5)\n * - Applies EMA update to memory_chunks.relevance_score\n * - Finds entity names mentioned in those chunks and updates kg_entities.feedback_weight\n *\n * EMA formula: new = old + alpha * (normalized_rating - old)\n * where normalized_rating = (rating - 1) / 4 → maps [1,5] to [0,1]\n * and alpha = 0.1\n *\n * Integration into search scoring:\n * final_score *= (0.5 + relevance_score)\n * This multiplier ranges from 0.5 (relevance_score=0) to 1.5 (relevance_score=1),\n * giving a ±50% boost/penalty based on accumulated feedback.\n */\n\nimport type { ToolResult } from \"./types.js\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\n\n// EMA learning rate\nconst FEEDBACK_ALPHA = 0.1;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MemoryFeedbackParams {\n /** Array of chunk IDs to apply feedback to */\n chunk_ids: string[];\n /** Rating from 1 (not relevant) to 5 (highly relevant) */\n rating: number;\n /** Optional tenant ID for entity feedback scoping. Default: \"default\" */\n tenant_id?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\n/**\n * Apply relevance feedback to memory chunks and associated KG entities.\n * Both memory_chunks.relevance_score and kg_entities.feedback_weight go\n * through the passed StorageBackend, so this works on either backend.\n *\n * @param backend StorageBackend for both the chunk and kg_entities portions\n * @param params Feedback parameters\n */\nexport async function toolMemoryFeedback(\n backend: StorageBackend,\n params: MemoryFeedbackParams\n): Promise<ToolResult> {\n try {\n if (!Array.isArray(params.chunk_ids) || params.chunk_ids.length === 0) {\n return {\n content: [{ type: \"text\", text: \"memory_feedback error: chunk_ids must be a non-empty array\" }],\n isError: true,\n };\n }\n\n const rating = params.rating;\n if (typeof rating !== \"number\" || rating < 1 || rating > 5) {\n return {\n content: [{ type: \"text\", text: \"memory_feedback error: rating must be a number between 1 and 5\" }],\n isError: true,\n };\n }\n\n // Normalize rating [1,5] → [0,1]\n const normalizedRating = (rating - 1) / 4;\n\n // Fetch current relevance scores for the given chunk IDs\n const chunks = await backend.getChunksForFeedback(params.chunk_ids);\n\n if (chunks.length === 0) {\n return {\n content: [{ type: \"text\", text: \"memory_feedback: no matching chunks found\" }],\n isError: true,\n };\n }\n\n let updatedChunks = 0;\n const combinedText: string[] = [];\n\n // Apply EMA update to each chunk's relevance_score\n for (const chunk of chunks) {\n const oldScore = chunk.relevanceScore ?? 0.5;\n const newScore = oldScore + FEEDBACK_ALPHA * (normalizedRating - oldScore);\n await backend.updateChunkRelevanceScore(chunk.id, newScore);\n updatedChunks++;\n combinedText.push(chunk.text);\n }\n\n // Find entity mentions in updated chunks and apply EMA to kg_entities.feedback_weight\n const tenantId = params.tenant_id ?? \"default\";\n const entities = await backend.listKgEntities(tenantId, undefined, 500);\n const text = combinedText.join(\"\\n\").toLowerCase();\n\n let updatedEntities = 0;\n for (const entity of entities) {\n if (text.includes(entity.name.toLowerCase())) {\n await backend.updateEntityFeedbackWeight(entity.entity_id, normalizedRating, FEEDBACK_ALPHA);\n updatedEntities++;\n }\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify({\n updated_chunks: updatedChunks,\n updated_entities: updatedEntities,\n rating,\n normalized_rating: normalizedRating,\n }),\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_feedback error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * kg-search.ts — Graph-completion retrieval (MR1).\n *\n * Implements graphCompletionSearch: a multi-phase retrieval strategy that\n * combines vector search with knowledge-graph neighborhood expansion.\n *\n * Algorithm:\n * Phase 1: Wide vector search (50 candidates) using queryVec\n * Phase 2: Extract entity mentions from retrieved chunks (substring match vs kg_entities)\n * Phase 3: BFS neighborhood expansion in kg_triples (1-2 hops from matched entities)\n * Phase 4: Re-rank all collected triples against query embedding using cosine similarity\n *\n * The result is a ranked list of KgTriple objects most relevant to the query,\n * augmented with graph-derived context that pure vector search would miss.\n */\n\nimport { cosineSimilarity, deserializeEmbedding } from \"./embeddings.js\";\nimport { getConfiguredBackend } from \"./backends/index.js\";\nimport type { SearchResult, SearchOptions } from \"./search.js\";\nimport type { KgTriple } from \"./kg.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface GraphCompletionResult {\n /** Ranked KG triples relevant to the query */\n triples: Array<KgTriple & { relevanceScore: number }>;\n /** Chunks from the initial wide vector search (Phase 1) */\n seedChunks: SearchResult[];\n /** Entity names that were matched and used for BFS expansion */\n expandedEntities: string[];\n}\n\nexport interface GraphCompletionOptions {\n /** Number of seed chunks to fetch in Phase 1. Default: 50 */\n seedCount?: number;\n /** BFS hop depth for neighborhood expansion. Default: 1 */\n hops?: number;\n /** Maximum triples to return after re-ranking. Default: 20 */\n maxTriples?: number;\n /** Postgres project_id filter for triples. */\n projectId?: number;\n /** Tenant ID for entity lookup in the storage backend. Default: \"default\" */\n tenantId?: string;\n /** StorageBackend search options to pass to Phase 1 */\n searchOpts?: SearchOptions;\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2: Entity mention extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Find entity names (from the kg_entities table) that appear\n * as substrings in the given text chunks.\n *\n * Returns a deduplicated list of matching entity names.\n */\nasync function extractEntityMentions(\n federation: StorageBackend,\n chunks: SearchResult[],\n tenantId = \"default\"\n): Promise<string[]> {\n // Load all entity names for this tenant (sorted by length desc so longer\n // entity names are matched before substrings)\n const entities = await federation.listKgEntities(tenantId, undefined, 500);\n if (entities.length === 0) return [];\n\n const combinedText = chunks.map((c) => c.snippet).join(\"\\n\").toLowerCase();\n const matched = new Set<string>();\n\n for (const entity of entities) {\n if (combinedText.includes(entity.name.toLowerCase())) {\n matched.add(entity.name);\n }\n }\n\n return Array.from(matched);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3: BFS neighborhood expansion\n// ---------------------------------------------------------------------------\n\n/**\n * Perform BFS over kg_triples to find neighbors of the given entity names.\n *\n * For each entity name, fetches triples where the entity is the subject\n * or the object (bi-directional expansion). Supports 1 or 2 hop depths.\n *\n * Returns all unique triples found in the BFS expansion.\n */\nasync function bfsExpand(\n federation: StorageBackend,\n entityNames: string[],\n hops: number,\n projectId?: number\n): Promise<KgTriple[]> {\n const seen = new Set<number>(); // triple IDs already included\n const tripleMap = new Map<number, KgTriple>();\n const frontier = new Set(entityNames);\n\n for (let hop = 0; hop < hops; hop++) {\n const nextFrontier = new Set<string>();\n\n for (const name of frontier) {\n // Expand as subject\n const asSubject = await federation.queryKgTriples({\n subject: name,\n project_id: projectId,\n });\n\n // Expand as object\n const asObject = await federation.queryKgTriples({\n object: name,\n project_id: projectId,\n });\n\n for (const triple of [...asSubject, ...asObject]) {\n if (!seen.has(triple.id)) {\n seen.add(triple.id);\n tripleMap.set(triple.id, triple);\n // Add neighbors to next frontier for deeper hops\n nextFrontier.add(triple.subject);\n nextFrontier.add(triple.object);\n }\n }\n }\n\n // Next hop frontier: only new names not already processed\n frontier.clear();\n for (const name of nextFrontier) {\n if (!entityNames.includes(name)) {\n frontier.add(name);\n entityNames.push(name);\n }\n }\n }\n\n return Array.from(tripleMap.values());\n}\n\n// ---------------------------------------------------------------------------\n// Phase 4: Re-rank triples against query embedding\n// ---------------------------------------------------------------------------\n\n/**\n * Score each triple by cosine similarity to the query embedding.\n *\n * The triple text representation is: \"subject predicate object\"\n * We embed this on-the-fly using generateEmbedding(), which may use a cache.\n *\n * Returns triples sorted by relevance score descending.\n */\nasync function rerankTriples(\n triples: KgTriple[],\n queryVec: Float32Array,\n maxTriples: number\n): Promise<Array<KgTriple & { relevanceScore: number }>> {\n if (triples.length === 0) return [];\n\n const embedder = getConfiguredBackend();\n const scored = await Promise.all(\n triples.map(async (triple) => {\n const text = `${triple.subject} ${triple.predicate} ${triple.object}`;\n try {\n const [vec] = await embedder.embed([text]);\n const score = cosineSimilarity(queryVec, vec);\n return { ...triple, relevanceScore: score };\n } catch {\n return { ...triple, relevanceScore: 0 };\n }\n })\n );\n\n return scored\n .sort((a, b) => b.relevanceScore - a.relevanceScore)\n .slice(0, maxTriples);\n}\n\n// ---------------------------------------------------------------------------\n// Main: graphCompletionSearch\n// ---------------------------------------------------------------------------\n\n/**\n * Graph-completion retrieval combining vector search with KG neighborhood BFS.\n *\n * @param federation Active StorageBackend (for chunk/entity/kg_triples lookup)\n * @param queryVec Pre-computed embedding for the search query\n * @param opts Configuration options\n */\nexport async function graphCompletionSearch(\n federation: StorageBackend,\n queryVec: Float32Array,\n opts: GraphCompletionOptions = {}\n): Promise<GraphCompletionResult> {\n const {\n seedCount = 50,\n hops = 1,\n maxTriples = 20,\n projectId,\n tenantId = \"default\",\n searchOpts,\n } = opts;\n\n // Phase 1: Wide vector search for seed chunks\n const seedSearchOpts: SearchOptions = {\n ...searchOpts,\n maxResults: seedCount,\n projectIds: projectId != null ? [projectId] : searchOpts?.projectIds,\n };\n\n let seedChunks: SearchResult[] = [];\n try {\n seedChunks = await federation.searchSemantic(queryVec, seedSearchOpts);\n } catch (e) {\n process.stderr.write(`[kg-search] Phase 1 seed search error: ${e}\\n`);\n }\n\n if (seedChunks.length === 0) {\n return { triples: [], seedChunks: [], expandedEntities: [] };\n }\n\n // Phase 2: Extract entity mentions from seed chunks\n const expandedEntities = await extractEntityMentions(federation, seedChunks, tenantId);\n\n if (expandedEntities.length === 0) {\n // No entities matched — return empty graph results (seed chunks still useful)\n return { triples: [], seedChunks, expandedEntities: [] };\n }\n\n // Phase 3: BFS neighborhood expansion in kg_triples\n let expandedTriples: KgTriple[] = [];\n try {\n expandedTriples = await bfsExpand(federation, [...expandedEntities], hops, projectId);\n } catch (e) {\n process.stderr.write(`[kg-search] Phase 3 BFS expansion error: ${e}\\n`);\n }\n\n if (expandedTriples.length === 0) {\n return { triples: [], seedChunks, expandedEntities };\n }\n\n // Phase 4: Re-rank triples against query embedding\n let rankedTriples: Array<KgTriple & { relevanceScore: number }> = [];\n try {\n rankedTriples = await rerankTriples(expandedTriples, queryVec, maxTriples);\n } catch (e) {\n process.stderr.write(`[kg-search] Phase 4 rerank error: ${e}\\n`);\n // Fall back to unranked triples\n rankedTriples = expandedTriples.slice(0, maxTriples).map((t) => ({\n ...t,\n relevanceScore: 0,\n }));\n }\n\n return { triples: rankedTriples, seedChunks, expandedEntities };\n}\n","/**\n * MCP tool handler: memory_kg_search\n *\n * Exposes graphCompletionSearch as an MCP tool (MR1 — graph-completion retrieval).\n *\n * Algorithm: wide vector search → entity mention extraction → BFS neighborhood\n * expansion in kg_triples → re-rank triples by cosine similarity to query.\n *\n * Requires the storage backend for chunk/entity lookups and kg_triples BFS\n * expansion.\n *\n * Returns ranked KG triples with relevanceScore, plus seed chunk metadata.\n */\n\nimport type { ToolResult } from \"./types.js\";\nimport type { StorageBackend } from \"../../storage/interface.js\";\nimport { graphCompletionSearch } from \"../../memory/kg-search.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" in a long-running daemon (prune-dist.mjs deletes superseded chunks).\nimport { requireQueryEmbedding } from \"../../memory/embedding-gate.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MemoryKgSearchParams {\n /** Free-text query — used to generate embedding for Phase 1 vector search. */\n query: string;\n /** Restrict seed vector search to a specific project ID. Optional. */\n project_id?: number;\n /** Number of seed chunks from Phase 1 vector search. Default: 50 */\n wide_k?: number;\n /** BFS hop depth for KG neighborhood expansion. Default: 1 */\n neighborhood_depth?: number;\n /** Maximum triples to return after Phase 4 re-ranking. Default: 20 */\n top_k?: number;\n /** Tenant ID for entity lookup in the storage backend. Default: \"default\" */\n tenant_id?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Tool handler\n// ---------------------------------------------------------------------------\n\n/**\n * Graph-completion search: vector search + KG neighborhood BFS.\n *\n * @param federation Active StorageBackend for chunk/entity/kg_triples operations\n * @param params Tool parameters\n */\nexport async function toolMemoryKgSearch(\n federation: StorageBackend,\n params: MemoryKgSearchParams\n): Promise<ToolResult> {\n try {\n if (!params.query || typeof params.query !== \"string\" || params.query.trim() === \"\") {\n return {\n content: [{ type: \"text\", text: \"memory_kg_search error: query is required\" }],\n isError: true,\n };\n }\n\n const queryVec = await requireQueryEmbedding(federation, params.query);\n\n const result = await graphCompletionSearch(federation, queryVec, {\n seedCount: params.wide_k ?? 50,\n hops: params.neighborhood_depth ?? 1,\n maxTriples: params.top_k ?? 20,\n projectId: params.project_id,\n tenantId: params.tenant_id ?? \"default\",\n });\n\n if (result.triples.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No KG triples found for query: \"${params.query}\"\\n` +\n `Seed chunks: ${result.seedChunks.length}, ` +\n `Expanded entities: ${result.expandedEntities.length}`,\n },\n ],\n };\n }\n\n const tripleLines = result.triples\n .map((t, i) =>\n `[${i + 1}] (score=${t.relevanceScore.toFixed(4)}) ${t.subject} — ${t.predicate} — ${t.object}`\n )\n .join(\"\\n\");\n\n const entityList = result.expandedEntities.slice(0, 10).join(\", \");\n const summary = [\n `Found ${result.triples.length} KG triple(s) for \"${params.query}\"`,\n `Seed chunks: ${result.seedChunks.length}`,\n `Expanded entities (${result.expandedEntities.length}): ${entityList}`,\n \"\",\n tripleLines,\n ].join(\"\\n\");\n\n return {\n content: [{ type: \"text\", text: summary }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `memory_kg_search error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n","/**\n * clusters.ts — graph_clusters endpoint handler\n *\n * Reuses the zettelThemes() agglomerative clustering algorithm and enriches\n * each cluster with observation-type statistics, avg_recency from member\n * timestamps, and helper flags for the Obsidian knowledge plugin.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphClustersParams {\n project_id?: number;\n min_size?: number;\n max_clusters?: number;\n lookback_days?: number;\n similarity_threshold?: number;\n}\n\nexport interface ClusterNode {\n id: number;\n label: string;\n size: number;\n folder_diversity: number;\n avg_recency: number;\n linked_ratio: number;\n dominant_observation_type: string;\n observation_type_counts: Record<string, number>;\n suggest_index_note: boolean;\n has_idea_note: boolean;\n notes: Array<{ vault_path: string; title: string; indexed_at: number }>;\n}\n\nexport interface GraphClustersResult {\n clusters: ClusterNode[];\n total_notes_analyzed: number;\n time_window: { from: number; to: number };\n}\n\n/**\n * Aggregate per-path observation type counts into cluster-level counts,\n * then pick the dominant type.\n */\nfunction aggregateObservationTypes(\n paths: string[],\n byPath: Map<string, Record<string, number>>\n): { dominant: string; counts: Record<string, number> } {\n const counts: Record<string, number> = {};\n for (const path of paths) {\n const pathCounts = byPath.get(path);\n if (!pathCounts) continue;\n for (const [type, n] of Object.entries(pathCounts)) {\n counts[type] = (counts[type] ?? 0) + n;\n }\n }\n\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n\n return { dominant, counts };\n}\n\n// ---------------------------------------------------------------------------\n// Link-based fallback clustering (wikilink connected components)\n// ---------------------------------------------------------------------------\n\nconst SKIP_PREFIXES = [\n \"Attachments/\", \"🗓️ Daily Notes/\", \"Copilot/copilot-conversations/\",\n \"Z - Zettelkasten/Tweets/\",\n];\n\n/**\n * Cluster vault notes by wikilink connectivity when embeddings aren't available.\n * Uses BFS to find connected components in the link graph, then picks the\n * largest components as clusters. Labels are derived from the most common\n * title words in each component.\n */\nasync function clusterByLinks(\n backend: StorageBackend,\n lookbackDays: number,\n minSize: number,\n maxClusters: number,\n): Promise<{ themes: Array<{ id: number; label: string; notes: Array<{ path: string; title: string | null }>; size: number; folderDiversity: number; avgRecency: number; linkedRatio: number; suggestIndexNote: boolean }>; totalNotesAnalyzed: number; timeWindow: { from: number; to: number } }> {\n const now = Date.now();\n const from = now - lookbackDays * 86400000;\n\n // Get recent notes\n const recentFiles = await backend.getRecentVaultFiles(from);\n const recentNotes = recentFiles.filter(f => f.vaultPath.endsWith(\".md\"));\n\n const noteMap = new Map<string, { title: string | null; indexed_at: number }>();\n for (const n of recentNotes) {\n noteMap.set(n.vaultPath, { title: n.title, indexed_at: n.indexedAt });\n }\n\n // Build adjacency list from vault_links (only for recent notes)\n const adj = new Map<string, Set<string>>();\n for (const path of noteMap.keys()) {\n if (!adj.has(path)) adj.set(path, new Set());\n }\n\n const linkGraph = await backend.getVaultLinkGraph();\n\n for (const { source_path, target_path } of linkGraph) {\n if (noteMap.has(source_path) && noteMap.has(target_path)) {\n adj.get(source_path)!.add(target_path);\n adj.get(target_path)!.add(source_path);\n }\n }\n\n // Remove hub nodes before BFS\n const degrees = [...adj.entries()].map(([p, s]) => ({ path: p, degree: s.size }));\n degrees.sort((a, b) => b.degree - a.degree);\n const hubThreshold = Math.max(10, degrees[Math.floor(degrees.length * 0.05)]?.degree ?? 10);\n const hubNodes = new Set<string>();\n for (const { path, degree } of degrees) {\n if (degree >= hubThreshold) hubNodes.add(path);\n else break;\n }\n\n for (const hub of hubNodes) {\n adj.delete(hub);\n }\n for (const [, neighbors] of adj) {\n for (const hub of hubNodes) {\n neighbors.delete(hub);\n }\n }\n\n // BFS connected components\n const visited = new Set<string>();\n const components: string[][] = [];\n\n for (const path of noteMap.keys()) {\n if (visited.has(path) || hubNodes.has(path)) continue;\n if (SKIP_PREFIXES.some(p => path.startsWith(p))) { visited.add(path); continue; }\n const component: string[] = [];\n const queue = [path];\n visited.add(path);\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n component.push(current);\n const neighbors = adj.get(current);\n if (!neighbors) continue;\n for (const neighbor of neighbors) {\n if (!visited.has(neighbor) && !SKIP_PREFIXES.some(p => neighbor.startsWith(p))) {\n visited.add(neighbor);\n queue.push(neighbor);\n }\n }\n }\n\n if (component.length >= minSize) {\n components.push(component);\n }\n }\n\n components.sort((a, b) => b.length - a.length);\n const topComponents = components.slice(0, maxClusters);\n\n // STOP_WORDS imported from utils/stop-words.ts (module-level import)\n\n function generateLinkLabel(paths: string[]): string {\n const wordCounts = new Map<string, number>();\n for (const p of paths) {\n const title = noteMap.get(p)?.title;\n if (!title) continue;\n const words = title.toLowerCase().replace(/[^a-z0-9äöüàéèêëçñß\\s]/g, \" \").split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted.slice(0, 3).map(([w]) => w).join(\" / \") || \"Linked Notes\";\n }\n\n const themes = topComponents.map((component, idx) => {\n const notes = component.map(p => ({\n path: p,\n title: noteMap.get(p)?.title ?? null,\n }));\n const avgRecency = component.reduce((sum, p) => sum + (noteMap.get(p)?.indexed_at ?? 0), 0) / component.length;\n const uniqueFolders = new Set(component.map(p => p.split(\"/\")[0]));\n\n return {\n id: idx,\n label: generateLinkLabel(component),\n notes,\n size: component.length,\n folderDiversity: uniqueFolders.size / component.length,\n avgRecency,\n linkedRatio: 1.0,\n suggestIndexNote: component.length >= 10,\n };\n });\n\n return {\n themes,\n totalNotesAnalyzed: recentNotes.length,\n timeWindow: { from, to: now },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphClusters(\n backend: StorageBackend,\n params: GraphClustersParams\n): Promise<GraphClustersResult> {\n const minSize = params.min_size ?? 3;\n const maxClusters = params.max_clusters ?? 20;\n const lookbackDays = params.lookback_days ?? 90;\n\n const vaultProjectId = params.project_id ?? 0;\n\n if (!vaultProjectId) {\n throw new Error(\n \"graph_clusters: project_id is required (pass the vault project's numeric ID)\"\n );\n }\n\n const themeResult = await clusterByLinks(backend, lookbackDays, minSize, maxClusters);\n\n const allPaths = themeResult.themes.flatMap((t) => t.notes.map((n) => n.path));\n\n const observationsByPath = await backend.getObservationTypesForPaths(allPaths, params.project_id);\n\n // Fetch indexed_at timestamps for all notes in bulk\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const indexedAtMap = new Map<string, number>(fileRows.map(f => [f.vaultPath, f.indexedAt]));\n\n const clusters: ClusterNode[] = themeResult.themes.map((theme) => {\n const notePaths = theme.notes.map((n) => n.path);\n\n const notesWithTimestamps = theme.notes.map((n) => ({\n vault_path: n.path,\n title: n.title ?? n.path.split(\"/\").pop() ?? n.path,\n indexed_at: indexedAtMap.get(n.path) ?? 0,\n }));\n\n const avgRecency = theme.avgRecency;\n\n const { dominant, counts } = aggregateObservationTypes(\n notePaths,\n observationsByPath\n );\n\n return {\n id: theme.id,\n label: theme.label,\n size: theme.size,\n folder_diversity: theme.folderDiversity,\n avg_recency: avgRecency,\n linked_ratio: theme.linkedRatio,\n dominant_observation_type: dominant,\n observation_type_counts: counts,\n suggest_index_note: theme.suggestIndexNote,\n has_idea_note: false,\n notes: notesWithTimestamps,\n };\n });\n\n clusters.sort((a, b) => b.size - a.size);\n\n return {\n clusters: clusters.slice(0, maxClusters),\n total_notes_analyzed: themeResult.totalNotesAnalyzed,\n time_window: themeResult.timeWindow,\n };\n}\n","/**\n * neighborhood.ts — graph_neighborhood endpoint handler\n *\n * Given a set of vault note paths (typically the notes inside a cluster),\n * returns the individual note nodes and the wikilink edges between them.\n *\n * Optionally enriches with semantic edges computed from cosine similarity\n * between chunk embeddings stored in the federation database.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { deserializeEmbedding } from \"../memory/embeddings.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphNeighborhoodParams {\n /** Vault-relative paths of notes in the cluster */\n vault_paths: string[];\n /** Numeric PAI project ID */\n project_id: number;\n /** Whether to compute semantic similarity edges (default: false) */\n include_semantic_edges?: boolean;\n /** Cosine similarity threshold for semantic edges (default: 0.7) */\n semantic_threshold?: number;\n}\n\nexport interface NoteNode {\n vault_path: string;\n title: string;\n folder: string;\n observation_types: Record<string, number>;\n dominant_type: string;\n updated_at: number;\n word_count: number;\n}\n\nexport interface NoteEdge {\n source: string;\n target: string;\n type: \"wikilink\" | \"semantic\";\n weight: number;\n}\n\nexport interface GraphNeighborhoodResult {\n nodes: NoteNode[];\n edges: NoteEdge[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length || a.length === 0) return 0;\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n if (normA === 0 || normB === 0) return 0;\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n\nfunction dominantType(counts: Record<string, number>): string {\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n return dominant;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphNeighborhood(\n backend: StorageBackend,\n params: GraphNeighborhoodParams\n): Promise<GraphNeighborhoodResult> {\n const vaultPaths = params.vault_paths ?? [];\n if (vaultPaths.length === 0) {\n return { nodes: [], edges: [] };\n }\n\n const includeSemanticEdges = params.include_semantic_edges ?? false;\n const semanticThreshold = params.semantic_threshold ?? 0.7;\n\n // -------------------------------------------------------------------------\n // 1. Fetch node metadata from vault_files\n // -------------------------------------------------------------------------\n\n const fileRows = await backend.getVaultFilesByPaths(vaultPaths);\n\n const fileIndex = new Map<string, { vaultPath: string; title: string | null; indexedAt: number }>();\n for (const row of fileRows) {\n fileIndex.set(row.vaultPath, row);\n }\n\n // -------------------------------------------------------------------------\n // 2. Fetch observation types (Postgres if available)\n // -------------------------------------------------------------------------\n\n const observationsByPath = await backend.getObservationTypesForPaths(vaultPaths, params.project_id);\n\n // -------------------------------------------------------------------------\n // 3. Build NoteNode array\n // -------------------------------------------------------------------------\n\n const nodes: NoteNode[] = vaultPaths.map((vp) => {\n const fileRow = fileIndex.get(vp);\n const fileName = vp.split(\"/\").pop() ?? vp;\n const rawTitle = fileRow?.title ?? fileName.replace(/\\.md$/i, \"\");\n\n const obsCounts = observationsByPath.get(vp) ?? {};\n\n return {\n vault_path: vp,\n title: rawTitle,\n folder: folderFromPath(vp),\n observation_types: obsCounts,\n dominant_type: dominantType(obsCounts),\n updated_at: fileRow?.indexedAt ?? 0,\n word_count: 0,\n };\n });\n\n // -------------------------------------------------------------------------\n // 4. Fetch wikilink edges between the provided paths\n // -------------------------------------------------------------------------\n\n const pathSet = new Set(vaultPaths);\n const linkRows = await backend.getVaultLinksFromPaths(vaultPaths);\n\n const edges: NoteEdge[] = [];\n\n for (const row of linkRows) {\n if (!row.targetPath || !pathSet.has(row.targetPath)) continue;\n\n edges.push({\n source: row.sourcePath,\n target: row.targetPath,\n type: \"wikilink\",\n weight: 1.0,\n });\n }\n\n // -------------------------------------------------------------------------\n // 5. Optional: semantic edges\n // -------------------------------------------------------------------------\n\n if (includeSemanticEdges && vaultPaths.length > 1) {\n // Fetch mean embeddings for all paths\n const embeddings = new Map<string, number[]>();\n for (const vp of vaultPaths) {\n const chunkRows = await backend.getChunksForPath(params.project_id, vp);\n const embRows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) continue;\n\n let vecLen = 0;\n const vectors: Float32Array[] = [];\n\n for (const row of embRows) {\n const arr = deserializeEmbedding(row.embedding);\n if (vecLen === 0) vecLen = arr.length;\n if (arr.length === vecLen) vectors.push(arr);\n }\n\n if (vectors.length === 0 || vecLen === 0) continue;\n\n const mean = new Array<number>(vecLen).fill(0);\n for (const vec of vectors) {\n for (let i = 0; i < vecLen; i++) {\n mean[i] += vec[i];\n }\n }\n for (let i = 0; i < vecLen; i++) {\n mean[i] /= vectors.length;\n }\n embeddings.set(vp, mean);\n }\n\n const existingEdgeKeys = new Set<string>(\n edges.map((e) => `${e.source}|||${e.target}`)\n );\n\n const pathsWithEmbeddings = Array.from(embeddings.keys());\n for (let i = 0; i < pathsWithEmbeddings.length; i++) {\n for (let j = i + 1; j < pathsWithEmbeddings.length; j++) {\n const pathA = pathsWithEmbeddings[i];\n const pathB = pathsWithEmbeddings[j];\n\n const vecA = embeddings.get(pathA)!;\n const vecB = embeddings.get(pathB)!;\n\n const sim = cosineSimilarity(vecA, vecB);\n if (sim < semanticThreshold) continue;\n\n const keyAB = `${pathA}|||${pathB}`;\n const keyBA = `${pathB}|||${pathA}`;\n if (existingEdgeKeys.has(keyAB) || existingEdgeKeys.has(keyBA)) continue;\n\n edges.push({\n source: pathA,\n target: pathB,\n type: \"semantic\",\n weight: sim,\n });\n existingEdgeKeys.add(keyAB);\n }\n }\n }\n\n return { nodes, edges };\n}\n","/**\n * note-context.ts — graph_note_context endpoint handler\n *\n * Given a single vault note path, returns ALL notes linked to or from it\n * across the entire vault (1-hop neighbourhood), plus the edges.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphNoteContextParams {\n vault_path: string;\n project_id: number;\n max_neighbors?: number;\n include_backlinks?: boolean;\n include_outlinks?: boolean;\n}\n\nexport interface NoteNode {\n vault_path: string;\n title: string;\n folder: string;\n observation_types: Record<string, number>;\n dominant_type: string;\n updated_at: number;\n word_count: number;\n}\n\nexport interface NoteEdge {\n source: string;\n target: string;\n type: \"wikilink\" | \"semantic\";\n weight: number;\n}\n\nexport interface GraphNoteContextResult {\n focal: NoteNode;\n neighbors: NoteNode[];\n edges: NoteEdge[];\n cluster_membership: Record<string, number>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction dominantType(counts: Record<string, number>): string {\n let best = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n best = type;\n }\n }\n return best;\n}\n\nfunction buildNoteNode(\n vaultPath: string,\n fileIndex: Map<string, { title: string | null; indexedAt: number }>,\n obsByPath: Map<string, Record<string, number>>\n): NoteNode {\n const fileRow = fileIndex.get(vaultPath);\n const fileName = vaultPath.split(\"/\").pop() ?? vaultPath;\n const rawTitle = fileRow?.title ?? fileName.replace(/\\.md$/i, \"\");\n const obsCounts = obsByPath.get(vaultPath) ?? {};\n\n return {\n vault_path: vaultPath,\n title: rawTitle,\n folder: folderFromPath(vaultPath),\n observation_types: obsCounts,\n dominant_type: dominantType(obsCounts),\n updated_at: fileRow?.indexedAt ?? 0,\n word_count: 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphNoteContext(\n backend: StorageBackend,\n params: GraphNoteContextParams\n): Promise<GraphNoteContextResult> {\n const focalPath = params.vault_path;\n if (!focalPath) {\n throw new Error(\"graph_note_context: vault_path is required\");\n }\n\n const maxNeighbors = params.max_neighbors ?? 50;\n const includeBacklinks = params.include_backlinks !== false;\n const includeOutlinks = params.include_outlinks !== false;\n\n // -------------------------------------------------------------------------\n // 1. Collect 1-hop neighbor paths via vault_links\n // -------------------------------------------------------------------------\n\n const neighborPaths = new Set<string>();\n const rawEdges: Array<{ source: string; target: string }> = [];\n\n if (includeOutlinks) {\n const outLinks = await backend.getLinksFromSource(focalPath);\n for (const link of outLinks) {\n if (!link.targetPath) continue;\n neighborPaths.add(link.targetPath);\n rawEdges.push({ source: focalPath, target: link.targetPath });\n }\n }\n\n if (includeBacklinks) {\n const inLinks = await backend.getLinksToTarget(focalPath);\n for (const link of inLinks) {\n neighborPaths.add(link.sourcePath);\n rawEdges.push({ source: link.sourcePath, target: focalPath });\n }\n }\n\n // Cap neighbors at max_neighbors, keeping the most-linked ones\n let neighborPathList = Array.from(neighborPaths);\n if (neighborPathList.length > maxNeighbors) {\n const linkCount = new Map<string, number>();\n for (const e of rawEdges) {\n const neighbor = e.source === focalPath ? e.target : e.source;\n linkCount.set(neighbor, (linkCount.get(neighbor) ?? 0) + 1);\n }\n neighborPathList = neighborPathList\n .sort((a, b) => (linkCount.get(b) ?? 0) - (linkCount.get(a) ?? 0))\n .slice(0, maxNeighbors);\n }\n\n const retainedSet = new Set(neighborPathList);\n const retainedEdges = rawEdges.filter((e) => {\n const neighbor = e.source === focalPath ? e.target : e.source;\n return retainedSet.has(neighbor);\n });\n\n // -------------------------------------------------------------------------\n // 2. Fetch vault_files metadata for focal + all neighbors\n // -------------------------------------------------------------------------\n\n const allPaths = [focalPath, ...neighborPathList];\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const fileIndex = new Map<string, { title: string | null; indexedAt: number }>(\n fileRows.map(f => [f.vaultPath, { title: f.title, indexedAt: f.indexedAt }])\n );\n\n // -------------------------------------------------------------------------\n // 3. Observation type enrichment (Postgres if available)\n // -------------------------------------------------------------------------\n\n const obsByPath = await backend.getObservationTypesForPaths(allPaths, params.project_id);\n\n // -------------------------------------------------------------------------\n // 4. Build focal NoteNode\n // -------------------------------------------------------------------------\n\n const focal = buildNoteNode(focalPath, fileIndex, obsByPath);\n\n // -------------------------------------------------------------------------\n // 5. Build neighbor NoteNode array\n // -------------------------------------------------------------------------\n\n const neighbors: NoteNode[] = neighborPathList.map((vp) =>\n buildNoteNode(vp, fileIndex, obsByPath)\n );\n\n // -------------------------------------------------------------------------\n // 6. Deduplicate edges\n // -------------------------------------------------------------------------\n\n const edgeKeys = new Set<string>();\n const edges: NoteEdge[] = [];\n for (const e of retainedEdges) {\n const key = `${e.source}|||${e.target}`;\n if (!edgeKeys.has(key)) {\n edgeKeys.add(key);\n edges.push({\n source: e.source,\n target: e.target,\n type: \"wikilink\",\n weight: 1.0,\n });\n }\n }\n\n return {\n focal,\n neighbors,\n edges,\n cluster_membership: {},\n };\n}\n","/**\n * trace.ts — graph_trace endpoint handler\n *\n * Given a topic/keyword query, searches vault notes for appearances of that topic\n * and returns a chronological timeline showing how the idea evolved over time.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphTraceParams {\n query: string;\n project_id: number;\n max_results?: number;\n lookback_days?: number;\n}\n\nexport interface TraceEntry {\n vault_path: string;\n title: string;\n folder: string;\n indexed_at: number;\n snippet: string;\n dominant_type: string;\n}\n\nexport interface TraceConnection {\n from_path: string;\n to_path: string;\n type: \"temporal\" | \"wikilink\";\n}\n\nexport interface GraphTraceResult {\n query: string;\n entries: TraceEntry[];\n connections: TraceConnection[];\n time_span: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction extractSnippet(text: string, query: string): string {\n const lowerText = text.toLowerCase();\n const lowerQuery = query.toLowerCase();\n const idx = lowerText.indexOf(lowerQuery);\n if (idx === -1) {\n return text.slice(0, 160).trimEnd() + (text.length > 160 ? \"…\" : \"\");\n }\n\n const CONTEXT = 70;\n const start = Math.max(0, idx - CONTEXT);\n const end = Math.min(text.length, idx + query.length + CONTEXT);\n\n let snippet = text.slice(start, end).replace(/\\s+/g, \" \").trim();\n if (start > 0) snippet = \"…\" + snippet;\n if (end < text.length) snippet = snippet + \"…\";\n\n return snippet;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphTrace(\n backend: StorageBackend,\n params: GraphTraceParams\n): Promise<GraphTraceResult> {\n const query = (params.query ?? \"\").trim();\n if (!query) {\n return { query: \"\", entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n const maxResults = params.max_results ?? 30;\n const lookbackDays = params.lookback_days ?? 365;\n const cutoffTimestamp = Math.floor(Date.now() / 1000) - lookbackDays * 86400;\n\n // ---------------------------------------------------------------------------\n // Step 1: Collect matching vault paths via multiple search strategies\n // ---------------------------------------------------------------------------\n\n const matchedPaths = new Map<string, string>(); // path → best snippet text\n\n // Strategy A: title / alias match in vault_name_index\n try {\n const namePaths = await backend.searchVaultNameIndex(query, 100);\n for (const vaultPath of namePaths) {\n if (!matchedPaths.has(vaultPath)) {\n matchedPaths.set(vaultPath, \"\");\n }\n }\n } catch {\n // vault_name_index may not exist in all schema versions\n }\n\n // Strategy B: content search in memory_chunks\n try {\n const chunkRows = await backend.searchChunksByText(params.project_id, query, 200);\n for (const row of chunkRows) {\n if (!matchedPaths.has(row.path)) {\n matchedPaths.set(row.path, row.text);\n } else if (!matchedPaths.get(row.path)) {\n matchedPaths.set(row.path, row.text);\n }\n }\n } catch {\n // Best-effort — continue with what we have\n }\n\n if (matchedPaths.size === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 2: Fetch vault_files metadata for all matched paths, filtered by cutoff\n // ---------------------------------------------------------------------------\n\n const allPaths = Array.from(matchedPaths.keys());\n let fileRows: Array<{ vaultPath: string; title: string | null; indexedAt: number }> = [];\n try {\n fileRows = await backend.getVaultFilesByPathsAfter(allPaths, cutoffTimestamp * 1000);\n // Sort chronologically\n fileRows.sort((a, b) => a.indexedAt - b.indexedAt);\n } catch {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n if (fileRows.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 3: Build TraceEntry array\n // ---------------------------------------------------------------------------\n\n const entries: TraceEntry[] = fileRows.slice(0, maxResults).map((row) => {\n const fileName = row.vaultPath.split(\"/\").pop() ?? row.vaultPath;\n const title = row.title ?? fileName.replace(/\\.md$/i, \"\");\n const chunkText = matchedPaths.get(row.vaultPath) ?? \"\";\n const snippet = extractSnippet(chunkText, query);\n\n return {\n vault_path: row.vaultPath,\n title,\n folder: folderFromPath(row.vaultPath),\n indexed_at: row.indexedAt,\n snippet,\n dominant_type: \"unknown\",\n };\n });\n\n if (entries.length === 0) {\n return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n }\n\n // ---------------------------------------------------------------------------\n // Step 4: Build connections\n // ---------------------------------------------------------------------------\n\n const connections: TraceConnection[] = [];\n const entryPathSet = new Set(entries.map((e) => e.vault_path));\n\n // Temporal edges: consecutive entries (oldest → next)\n for (let i = 0; i < entries.length - 1; i++) {\n connections.push({\n from_path: entries[i].vault_path,\n to_path: entries[i + 1].vault_path,\n type: \"temporal\",\n });\n }\n\n // Wikilink edges\n try {\n const entryPaths = entries.map(e => e.vault_path);\n const linkRows = await backend.getVaultLinksFromPaths(entryPaths);\n\n const wikiEdgeKeys = new Set<string>();\n for (const row of linkRows) {\n if (!row.targetPath) continue;\n if (!entryPathSet.has(row.targetPath)) continue;\n\n const key = `${row.sourcePath}|||${row.targetPath}`;\n if (wikiEdgeKeys.has(key)) continue;\n wikiEdgeKeys.add(key);\n\n connections.push({\n from_path: row.sourcePath,\n to_path: row.targetPath,\n type: \"wikilink\",\n });\n }\n } catch {\n // vault_links may not exist — temporal edges are sufficient\n }\n\n // ---------------------------------------------------------------------------\n // Step 5: Compute time span\n // ---------------------------------------------------------------------------\n\n const timestamps = entries.map((e) => e.indexed_at).filter((t) => t > 0);\n const timeFrom = timestamps.length > 0 ? Math.min(...timestamps) : 0;\n const timeTo = timestamps.length > 0 ? Math.max(...timestamps) : 0;\n\n return {\n query,\n entries,\n connections,\n time_span: { from: timeFrom, to: timeTo },\n };\n}\n","/**\n * latent-ideas.ts — graph_latent_ideas and idea_materialize endpoint handlers\n *\n * \"Latent ideas\" are recurring themes in the vault that exist as embedding\n * clusters but have NO dedicated note written about them yet. PAI surfaces\n * these by running the same agglomerative clustering used by graph_clusters /\n * zettelThemes and then filtering OUT any cluster whose label is well-matched\n * by an existing note title.\n *\n * The materialize endpoint writes a new Markdown note to the vault filesystem\n * and returns its content so the plugin can open it immediately.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { TITLE_STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { zettelThemes } from \"../zettelkasten/themes.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphLatentIdeasParams {\n project_id: number;\n /** Minimum notes in a cluster (default: 3) */\n min_cluster_size?: number;\n /** Cap on returned ideas (default: 15) */\n max_ideas?: number;\n /** How far back to look in days (default: 180) */\n lookback_days?: number;\n /** Cosine similarity clustering threshold (default: 0.65) */\n similarity_threshold?: number;\n}\n\nexport interface LatentIdeaSourceNote {\n vault_path: string;\n title: string;\n /** How strongly this note relates to the theme (0-1) */\n relevance: number;\n}\n\nexport interface LatentIdea {\n id: number;\n /** Auto-generated cluster label from zettelThemes */\n label: string;\n /** Number of notes touching this theme */\n size: number;\n /** 0-1, how likely this is a real coherent idea */\n confidence: number;\n /** Notes that contribute to this theme */\n source_notes: LatentIdeaSourceNote[];\n /** Cleaned-up version of label for a potential note title */\n suggested_title: string;\n /** Most common folder among source notes */\n suggested_folder: string;\n /** Number of distinct session date-folders (e.g. \"2026/03\") touching this theme */\n sessions_count: number;\n}\n\nexport interface GraphLatentIdeasResult {\n ideas: LatentIdea[];\n total_clusters_analyzed: number;\n /** How many clusters already have a matching note (excluded from results) */\n materialized_count: number;\n}\n\n// ---------------------------------------------------------------------------\n// Materialize params / result\n// ---------------------------------------------------------------------------\n\nexport interface IdeaMaterializeParams {\n idea_label: string;\n /** User-chosen title for the new note */\n title: string;\n /** Vault-relative folder path where the note should be created */\n folder: string;\n /** Vault-relative paths of the source notes to link from the new note */\n source_paths: string[];\n project_id: number;\n}\n\nexport interface IdeaMaterializeResult {\n /** Vault-relative path of the created note */\n vault_path: string;\n /** Generated markdown content */\n content: string;\n /** Number of wikilinks inserted */\n links_created: number;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: check if a cluster already has a matching note\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true when any existing vault note title closely matches the cluster\n * label — meaning a dedicated note already exists for this topic.\n *\n * Matching strategy (simple, fast, no embeddings needed):\n * 1. Lowercase both sides and split into words.\n * 2. Remove stop words from the label words.\n * 3. If ≥ 60% of the significant label words appear in a note title → match.\n */\n// TITLE_STOP_WORDS imported from utils/stop-words.ts\n\nfunction labelMatchesTitle(label: string, title: string): boolean {\n const labelWords = label\n .toLowerCase()\n .split(/[\\s\\-_/]+/)\n .filter((w) => w.length > 2 && !TITLE_STOP_WORDS.has(w));\n\n if (labelWords.length === 0) return false;\n\n const titleLower = title.toLowerCase();\n const matchCount = labelWords.filter((w) => titleLower.includes(w)).length;\n return matchCount / labelWords.length >= 0.6;\n}\n\n/**\n * Check whether any note indexed in the vault has a title matching the label.\n * Fetches all vault file rows via StorageBackend for efficiency.\n */\nasync function clusterHasMatchingNote(\n backend: StorageBackend,\n label: string,\n notePaths: string[]\n): Promise<boolean> {\n // First check the notes already in the cluster themselves — if any cluster\n // member's title matches the label it IS the index note → materialized.\n const pathSet = new Set(notePaths);\n\n // Fetch all vault files (bounded — vault rarely > 50k notes)\n const rows = await backend.getAllVaultFiles();\n\n for (const row of rows) {\n if (!row.title) continue;\n // Skip notes already counted inside the cluster — they don't count as\n // \"dedicated notes\"; we only skip a cluster if a SEPARATE note exists.\n if (pathSet.has(row.vaultPath)) continue;\n if (labelMatchesTitle(label, row.title)) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: generate a clean suggested title\n// ---------------------------------------------------------------------------\n\nfunction toSuggestedTitle(label: string): string {\n // Remove leading/trailing whitespace, capitalize each word, remove stop words\n // that are all-lowercase at the start of the title.\n const words = label\n .trim()\n .split(/\\s+/)\n .map((w, i) => {\n const lower = w.toLowerCase();\n // Drop leading stop words (but keep if they're the only word)\n if (i === 0 && TITLE_STOP_WORDS.has(lower) && label.trim().split(/\\s+/).length > 1) {\n return \"\";\n }\n return w.charAt(0).toUpperCase() + w.slice(1);\n })\n .filter(Boolean);\n\n return words.join(\" \") || label;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: find most common folder\n// ---------------------------------------------------------------------------\n\nfunction mostCommonFolder(vaultPaths: string[]): string {\n const counts = new Map<string, number>();\n for (const p of vaultPaths) {\n const parts = p.split(\"/\");\n const folder = parts.length > 1 ? parts.slice(0, -1).join(\"/\") : \"\";\n counts.set(folder, (counts.get(folder) ?? 0) + 1);\n }\n\n let best = \"\";\n let bestCount = 0;\n for (const [folder, count] of counts) {\n if (count > bestCount) {\n bestCount = count;\n best = folder;\n }\n }\n return best;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: count distinct session date-folders\n// ---------------------------------------------------------------------------\n\n/**\n * Heuristic: vault notes are often stored in date-based folders like\n * \"2026/03/15\" or \"Daily/2026-03\". We extract the first numeric path\n * segment that looks like a year (2020-2030) and group by year+month.\n *\n * Falls back to counting distinct top-level folders.\n */\nfunction countDistinctSessions(vaultPaths: string[]): number {\n const sessions = new Set<string>();\n const yearMonthRe = /\\b(202\\d)\\D?(0[1-9]|1[0-2])\\b/;\n\n for (const p of vaultPaths) {\n const m = yearMonthRe.exec(p);\n if (m) {\n sessions.add(`${m[1]}-${m[2]}`);\n } else {\n // Fallback: use top-level folder as a proxy for \"session bucket\"\n const topFolder = p.split(\"/\")[0];\n sessions.add(topFolder);\n }\n }\n return sessions.size;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: calculate confidence score\n// ---------------------------------------------------------------------------\n\n/**\n * Confidence combines:\n * - Cluster size (normalized, capped at 20 for max contribution)\n * - Folder diversity (0-1 already)\n * - Sessions count (normalized, capped at 5)\n *\n * Formula: 0.4 * sizeScore + 0.35 * folderDiversity + 0.25 * sessionScore\n */\nfunction calcConfidence(\n size: number,\n folderDiversity: number,\n sessionsCount: number\n): number {\n const sizeScore = Math.min(size / 20, 1.0);\n const sessionScore = Math.min(sessionsCount / 5, 1.0);\n const raw = 0.4 * sizeScore + 0.35 * folderDiversity + 0.25 * sessionScore;\n return Math.round(raw * 100) / 100;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler: graph_latent_ideas\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphLatentIdeas(\n backend: StorageBackend,\n params: GraphLatentIdeasParams\n): Promise<GraphLatentIdeasResult> {\n const minClusterSize = params.min_cluster_size ?? 3;\n const maxIdeas = params.max_ideas ?? 15;\n const lookbackDays = params.lookback_days ?? 180;\n const similarityThreshold = params.similarity_threshold ?? 0.65;\n\n const { project_id: vaultProjectId } = params;\n if (!vaultProjectId) {\n throw new Error(\n \"graph_latent_ideas: project_id is required (pass the vault project's numeric ID)\"\n );\n }\n\n // Run the same clustering algorithm used by graph_clusters\n const themeResult = await zettelThemes(backend, {\n vaultProjectId,\n lookbackDays,\n minClusterSize,\n maxThemes: maxIdeas * 3, // Over-fetch — many will be filtered as materialized\n similarityThreshold,\n });\n\n const ideas: LatentIdea[] = [];\n let materializedCount = 0;\n\n for (const theme of themeResult.themes) {\n const notePaths = theme.notes.map((n) => n.path);\n\n // Check if a dedicated note already exists for this theme\n if (await clusterHasMatchingNote(backend, theme.label, notePaths)) {\n materializedCount++;\n continue;\n }\n\n // This is a latent idea — no dedicated note exists yet\n const suggestedFolder = mostCommonFolder(notePaths);\n const sessionsCount = countDistinctSessions(notePaths);\n const confidence = calcConfidence(theme.size, theme.folderDiversity, sessionsCount);\n\n // Build source notes with relevance scores\n // Relevance is approximated by position in cluster (centroid-closest first)\n // zettelThemes returns notes in no guaranteed order; assign uniform relevance\n // decreasing from 1.0 to 0.5 across the list.\n const sourceNotes: LatentIdeaSourceNote[] = theme.notes.map((n, idx) => ({\n vault_path: n.path,\n title: n.title ?? n.path.split(\"/\").pop()?.replace(/\\.md$/i, \"\") ?? n.path,\n relevance: Math.round((1.0 - (idx / Math.max(theme.notes.length - 1, 1)) * 0.5) * 100) / 100,\n }));\n\n ideas.push({\n id: theme.id,\n label: theme.label,\n size: theme.size,\n confidence,\n source_notes: sourceNotes,\n suggested_title: toSuggestedTitle(theme.label),\n suggested_folder: suggestedFolder,\n sessions_count: sessionsCount,\n });\n\n if (ideas.length >= maxIdeas) break;\n }\n\n // Sort by confidence descending\n ideas.sort((a, b) => b.confidence - a.confidence);\n\n return {\n ideas,\n total_clusters_analyzed: themeResult.themes.length + materializedCount,\n materialized_count: materializedCount,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Materialize handler: idea_materialize\n// ---------------------------------------------------------------------------\n\nexport function handleIdeaMaterialize(\n params: IdeaMaterializeParams,\n vaultPath: string\n): IdeaMaterializeResult {\n const { idea_label, title, folder, source_paths } = params;\n\n // Sanitize filename: replace characters illegal in filenames\n const safeTitle = title.replace(/[/\\\\:*?\"<>|]/g, \"-\");\n const fileName = `${safeTitle}.md`;\n\n // Vault-relative path (forward slashes, no leading slash)\n const relFolder = folder.replace(/^\\/+|\\/+$/g, \"\");\n const vault_path = relFolder ? `${relFolder}/${fileName}` : fileName;\n\n // Absolute filesystem path\n const absPath = join(vaultPath, vault_path);\n const absDir = dirname(absPath);\n\n // Build wikilinks from source_paths\n const wikilinks = source_paths\n .map((p) => {\n // Derive a display name: filename without extension\n const name = p.split(\"/\").pop()?.replace(/\\.md$/i, \"\") ?? p;\n // Relative wikilink — use just the filename (Obsidian resolves by title)\n return `- [[${name}]]`;\n })\n .join(\"\\n\");\n\n const links_created = source_paths.length;\n\n const content = [\n `# ${title}`,\n \"\",\n `*Materialized from latent idea: \"${idea_label}\"*`,\n `*Sources: ${links_created} notes*`,\n \"\",\n \"## Related Notes\",\n \"\",\n wikilinks || \"*(no source notes)*\",\n \"\",\n \"## Notes\",\n \"\",\n \"<!-- Add your thoughts about this idea here -->\",\n \"\",\n ].join(\"\\n\");\n\n // Write the file (create parent directories as needed)\n mkdirSync(absDir, { recursive: true });\n writeFileSync(absPath, content, \"utf-8\");\n\n return {\n vault_path,\n content,\n links_created,\n };\n}\n","/**\n * Tool dispatcher — maps IPC method names to PAI tool functions.\n */\n\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolMemoryOutline,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolProjectTodo,\n toolSessionRoute,\n toolMemoryWakeup,\n toolMemoryTaxonomy,\n toolMemoryFeedback,\n toolMemoryKgSearch,\n} from \"../../mcp/tools.js\";\nimport { detectTopicShift } from \"../../topics/detector.js\";\nimport { registryBackend, storageBackend, daemonConfig } from \"./state.js\";\n// Static on purpose: a lazy import() of a dist chunk first hit after a rebuild\n// throws \"Cannot find module\" (prune-dist.mjs deletes superseded chunks).\nimport { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities, toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions, toolMemoryTunnels } from \"../../mcp/tools.js\";\nimport { handleGraphClusters } from \"../../graph/clusters.js\";\nimport { handleGraphNeighborhood } from \"../../graph/neighborhood.js\";\nimport { handleGraphNoteContext } from \"../../graph/note-context.js\";\nimport { handleGraphTrace } from \"../../graph/trace.js\";\nimport { handleGraphLatentIdeas, handleIdeaMaterialize } from \"../../graph/latent-ideas.js\";\n\n/**\n * Dispatch an IPC tool call to the appropriate tool function.\n * Returns the tool result or throws on unknown/failed methods.\n */\nexport async function dispatchTool(\n method: string,\n params: Record<string, unknown>\n): Promise<unknown> {\n // Cast through unknown to satisfy TypeScript's strict overlap check on\n // Record<string, unknown> → specific param types. Runtime validation is\n // the responsibility of each tool function (they surface errors gracefully).\n const p = params as unknown;\n\n switch (method) {\n case \"memory_search\":\n return toolMemorySearch(registryBackend, storageBackend, p as Parameters<typeof toolMemorySearch>[2]);\n\n case \"memory_get\":\n return toolMemoryGet(registryBackend, p as Parameters<typeof toolMemoryGet>[1]);\n\n case \"memory_outline\":\n return toolMemoryOutline(registryBackend, p as Parameters<typeof toolMemoryOutline>[1]);\n\n case \"project_info\":\n return toolProjectInfo(registryBackend, p as Parameters<typeof toolProjectInfo>[1]);\n\n case \"project_list\":\n return toolProjectList(registryBackend, p as Parameters<typeof toolProjectList>[1]);\n\n case \"session_list\":\n return toolSessionList(registryBackend, p as Parameters<typeof toolSessionList>[1]);\n\n case \"registry_search\":\n return toolRegistrySearch(registryBackend, p as Parameters<typeof toolRegistrySearch>[1]);\n\n case \"project_detect\":\n return toolProjectDetect(p as Parameters<typeof toolProjectDetect>[0]);\n\n case \"project_health\":\n return toolProjectHealth(registryBackend, p as Parameters<typeof toolProjectHealth>[1]);\n\n case \"project_todo\":\n return toolProjectTodo(registryBackend, p as Parameters<typeof toolProjectTodo>[1]);\n\n case \"memory_wakeup\":\n return toolMemoryWakeup(registryBackend, p as Parameters<typeof toolMemoryWakeup>[1]);\n\n case \"memory_taxonomy\":\n return toolMemoryTaxonomy(registryBackend, storageBackend, p as Parameters<typeof toolMemoryTaxonomy>[2]);\n\n case \"topic_check\":\n return detectTopicShift(\n registryBackend,\n storageBackend,\n p as Parameters<typeof detectTopicShift>[2]\n );\n\n case \"session_auto_route\":\n return toolSessionRoute(\n registryBackend,\n storageBackend,\n p as Parameters<typeof toolSessionRoute>[2]\n );\n\n case \"zettel_explore\":\n case \"zettel_health\":\n case \"zettel_surprise\":\n case \"zettel_suggest\":\n case \"zettel_converse\":\n case \"zettel_themes\":\n case \"zettel_god_notes\":\n case \"zettel_communities\": {\n\n switch (method) {\n case \"zettel_explore\": return toolZettelExplore(storageBackend, p as Parameters<typeof toolZettelExplore>[1]);\n case \"zettel_health\": return toolZettelHealth(storageBackend, p as Parameters<typeof toolZettelHealth>[1]);\n case \"zettel_surprise\": return toolZettelSurprise(storageBackend, p as Parameters<typeof toolZettelSurprise>[1]);\n case \"zettel_suggest\": return toolZettelSuggest(storageBackend, p as Parameters<typeof toolZettelSuggest>[1]);\n case \"zettel_converse\": return toolZettelConverse(storageBackend, p as Parameters<typeof toolZettelConverse>[1]);\n case \"zettel_themes\": return toolZettelThemes(storageBackend, p as Parameters<typeof toolZettelThemes>[1]);\n case \"zettel_god_notes\": return toolZettelGodNotes(storageBackend, p as Parameters<typeof toolZettelGodNotes>[1]);\n case \"zettel_communities\": return toolZettelCommunities(storageBackend, p as Parameters<typeof toolZettelCommunities>[1]);\n }\n break;\n }\n\n case \"graph_clusters\": {\n return handleGraphClusters(storageBackend, p as Parameters<typeof handleGraphClusters>[1]);\n }\n\n case \"graph_neighborhood\": {\n return handleGraphNeighborhood(storageBackend, p as Parameters<typeof handleGraphNeighborhood>[1]);\n }\n\n case \"graph_note_context\": {\n return handleGraphNoteContext(storageBackend, p as Parameters<typeof handleGraphNoteContext>[1]);\n }\n\n case \"graph_trace\": {\n return handleGraphTrace(storageBackend, p as Parameters<typeof handleGraphTrace>[1]);\n }\n\n case \"graph_latent_ideas\": {\n return handleGraphLatentIdeas(storageBackend, p as Parameters<typeof handleGraphLatentIdeas>[1]);\n }\n\n case \"idea_materialize\": {\n if (!daemonConfig.vaultPath) {\n throw new Error(\"idea_materialize requires vaultPath to be configured in the daemon config\");\n }\n return handleIdeaMaterialize(\n p as Parameters<typeof handleIdeaMaterialize>[0],\n daemonConfig.vaultPath\n );\n }\n\n case \"kg_add\":\n case \"kg_query\":\n case \"kg_invalidate\":\n case \"kg_contradictions\": {\n if (!storageBackend.supportsPostgresFeatures) {\n throw new Error(`${method} requires a Postgres storage backend`);\n }\n switch (method) {\n case \"kg_add\": return toolKgAdd(storageBackend, p as Parameters<typeof toolKgAdd>[1]);\n case \"kg_query\": return toolKgQuery(storageBackend, p as Parameters<typeof toolKgQuery>[1]);\n case \"kg_invalidate\": return toolKgInvalidate(storageBackend, p as Parameters<typeof toolKgInvalidate>[1]);\n case \"kg_contradictions\": return toolKgContradictions(storageBackend, p as Parameters<typeof toolKgContradictions>[1]);\n }\n break;\n }\n\n case \"memory_tunnels\": {\n return toolMemoryTunnels(registryBackend, storageBackend, p as Parameters<typeof toolMemoryTunnels>[2]);\n }\n\n case \"memory_feedback\": {\n // MR2: feedback weight loop — relevance_score/feedback_weight go through\n // the StorageBackend, so this works on either backend.\n return await toolMemoryFeedback(storageBackend, p as Parameters<typeof toolMemoryFeedback>[1]);\n }\n\n case \"memory_kg_search\": {\n // MR1: graph-completion retrieval — active storage backend for KG triple expansion\n if (!storageBackend.supportsPostgresFeatures) {\n throw new Error(\"memory_kg_search requires a Postgres storage backend for KG triple expansion\");\n }\n return await toolMemoryKgSearch(storageBackend, p as Parameters<typeof toolMemoryKgSearch>[1]);\n }\n\n default:\n throw new Error(`Unknown method: ${method}`);\n }\n}\n","/**\n * IPC request handler — processes all inbound IPC methods and sends responses.\n */\n\nimport type { Socket } from \"node:net\";\nimport type { IpcRequest, IpcResponse } from \"./types.js\";\nimport type { NotificationMode } from \"../../notifications/types.js\";\nimport {\n patchNotificationConfig,\n} from \"../../notifications/config.js\";\nimport { routeNotification } from \"../../notifications/router.js\";\nimport {\n registryBackend,\n storageBackend,\n daemonConfig,\n startTime,\n indexInProgress,\n lastIndexTime,\n embedInProgress,\n lastEmbedTime,\n vaultIndexInProgress,\n lastVaultIndexTime,\n passFailures,\n notificationConfig,\n setNotificationConfig,\n} from \"./state.js\";\nimport { runIndex, runVaultIndex } from \"./scheduler.js\";\nimport { dispatchTool } from \"./dispatcher.js\";\nimport { enqueue, getStats as getQueueStats } from \"../../daemon/work-queue.js\";\nimport { notifyNewWork } from \"../../daemon/work-queue-worker.js\";\nimport { getBackendOutage } from \"../../storage/outage.js\";\n\n// ---------------------------------------------------------------------------\n// Helper\n// ---------------------------------------------------------------------------\n\nexport function sendResponse(socket: Socket, response: IpcResponse): void {\n try {\n socket.write(JSON.stringify(response) + \"\\n\");\n } catch {\n // Socket may already be closed\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single IPC request.\n */\nexport async function handleRequest(\n request: IpcRequest,\n socket: Socket\n): Promise<void> {\n const { id, method, params } = request;\n\n // Special: status\n if (method === \"status\") {\n const dbStats = await (async () => {\n try {\n if (!storageBackend) return null;\n const fedStats = await storageBackend.getStats();\n const projects = await registryBackend.countProjects();\n return { files: fedStats.files, chunks: fedStats.chunks, projects };\n } catch {\n return null;\n }\n })();\n\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n uptime: Math.floor((Date.now() - startTime) / 1000),\n indexInProgress,\n lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,\n indexIntervalSecs: daemonConfig.indexIntervalSecs,\n embedInProgress,\n lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,\n embedIntervalSecs: daemonConfig.embedIntervalSecs,\n socketPath: daemonConfig.socketPath,\n storageBackend: storageBackend?.backendType ?? \"initializing\",\n db: dbStats,\n vaultIndexInProgress,\n lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,\n vaultPath: daemonConfig.vaultPath ?? null,\n workQueue: getQueueStats(),\n dashboard: daemonConfig.dashboard.enabled\n ? `http://${daemonConfig.dashboard.bind}:${daemonConfig.dashboard.port}/`\n : null,\n passFailures: Object.fromEntries(\n [...passFailures].map(([k, v]) => [k, { ...v, at: new Date(v.at).toISOString() }])\n ),\n // Null when the backend is answering. Present, this is the reason\n // everything else in this payload is stalled — and the reason status\n // used to report \"idle\" for two days while nothing progressed.\n backendOutage: getBackendOutage(),\n },\n });\n socket.end();\n return;\n }\n\n // Special: index_now — trigger immediate index (non-blocking response)\n if (method === \"index_now\") {\n runIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: vault_index_now — trigger immediate vault index (non-blocking response)\n if (method === \"vault_index_now\") {\n runVaultIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: notification_get_config\n if (method === \"notification_get_config\") {\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n config: notificationConfig,\n activeChannels: Object.entries(notificationConfig.channels)\n .filter(([ch, cfg]) => ch !== \"voice\" && (cfg as { enabled: boolean }).enabled)\n .map(([ch]) => ch),\n },\n });\n socket.end();\n return;\n }\n\n // Special: notification_set_config\n if (method === \"notification_set_config\") {\n try {\n const p = params as {\n mode?: NotificationMode;\n channels?: Record<string, unknown>;\n routing?: Record<string, unknown>;\n };\n const updated = patchNotificationConfig({\n mode: p.mode,\n channels: p.channels as Parameters<typeof patchNotificationConfig>[0][\"channels\"],\n routing: p.routing as Parameters<typeof patchNotificationConfig>[0][\"routing\"],\n });\n setNotificationConfig(updated);\n sendResponse(socket, { id, ok: true, result: { config: updated } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // Special: notification_send\n if (method === \"notification_send\") {\n const p = params as {\n event?: string;\n message?: string;\n title?: string;\n };\n\n if (!p.message) {\n sendResponse(socket, { id, ok: false, error: \"notification_send: message is required\" });\n socket.end();\n return;\n }\n\n const event = (p.event as Parameters<typeof routeNotification>[0][\"event\"]) ?? \"info\";\n\n routeNotification(\n { event, message: p.message, title: p.title },\n notificationConfig\n ).then((result) => {\n sendResponse(socket, { id, ok: true, result });\n socket.end();\n }).catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n socket.end();\n });\n return;\n }\n\n // ---- Observation methods (Postgres only) --------------------------------\n\n if (method === \"observation_store\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n type: string;\n title: string;\n narrative?: string;\n tool_name: string;\n tool_input_summary?: string;\n files_read?: string[];\n files_modified?: string[];\n concepts?: string[];\n content_hash?: string;\n cwd?: string;\n };\n\n const insertedId = await storageBackend.storeObservationWithProject(registryBackend, {\n session_id: p.session_id,\n type: p.type as \"decision\" | \"bugfix\" | \"feature\" | \"refactor\" | \"discovery\" | \"change\",\n title: p.title,\n narrative: p.narrative ?? null,\n tool_name: p.tool_name,\n tool_input_summary: p.tool_input_summary ?? undefined,\n files_read: p.files_read ?? [],\n files_modified: p.files_modified ?? [],\n concepts: p.concepts ?? [],\n cwd: p.cwd,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true, id: insertedId } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // ---- Skill telemetry (Postgres only) ------------------------------------\n\n if (method === \"skill_telemetry_record\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n skill_name: string;\n source?: string;\n scope?: string;\n cwd?: string;\n };\n\n let project_slug: string | null = null;\n if (p.cwd) {\n const row = await registryBackend.findProjectByCwdPrefix(p.cwd);\n if (row) project_slug = row.slug;\n }\n\n await storageBackend.recordSkillInvocation({\n skill_name: p.skill_name,\n source: p.source,\n scope: p.scope,\n project_slug,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"skill_telemetry_query\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { scope?: string; status?: string; limit?: number };\n const rows = await storageBackend.querySkillTelemetry({\n scope: p.scope,\n status: p.status,\n limit: p.limit,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_query\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_id?: number;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n const rows = await storageBackend.queryObservations({\n projectId: p.project_id,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit,\n offset: p.offset,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_recent\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { project_id?: number; cwd?: string; limit?: number };\n const limit = p.limit ?? 20;\n\n let resolvedProjectId = p.project_id;\n let resolvedProjectSlug: string | undefined;\n if (resolvedProjectId === undefined && p.cwd) {\n const row = await registryBackend.findProjectByCwdPrefix(p.cwd);\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n let rows;\n if (resolvedProjectId !== undefined) {\n rows = await storageBackend.queryRecentObservations(resolvedProjectId, limit);\n } else {\n rows = await storageBackend.queryObservations({ limit });\n }\n sendResponse(socket, { id, ok: true, result: { rows, project_slug: resolvedProjectSlug } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_list — alias for observation_query with project slug resolution\n if (method === \"observation_list\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_slug?: string;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n let projectId: number | undefined;\n if (p.project_slug) {\n const row = await registryBackend.getProjectBySlug(p.project_slug);\n projectId = row?.id;\n }\n\n const rows = await storageBackend.queryObservations({\n projectId,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit ?? 20,\n offset: p.offset ?? 0,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_stats — aggregate statistics\n if (method === \"observation_stats\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const stats = await storageBackend.getObservationStats();\n sendResponse(socket, { id, ok: true, result: stats });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"session_summary_store\") {\n if (!storageBackend?.supportsPostgresFeatures) {\n sendResponse(socket, { id, ok: false, error: \"Session summaries require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n project_id?: number;\n project_slug?: string;\n cwd?: string;\n request?: string;\n investigated?: string;\n learned?: string;\n completed?: string;\n next_steps?: string;\n observation_count?: number;\n };\n\n let resolvedProjectId = p.project_id ?? null;\n let resolvedProjectSlug = p.project_slug ?? null;\n if (resolvedProjectId === null && p.cwd) {\n const row = await registryBackend.findProjectByCwdPrefix(p.cwd);\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n await storageBackend.storeSessionSummary({\n session_id: p.session_id,\n project_id: resolvedProjectId,\n project_slug: resolvedProjectSlug,\n request: p.request ?? null,\n investigated: p.investigated ?? null,\n learned: p.learned ?? null,\n completed: p.completed ?? null,\n next_steps: p.next_steps ?? null,\n observation_count: p.observation_count ?? 0,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_enqueue — hooks push work items here instead of doing heavy work inline\n if (method === \"work_queue_enqueue\") {\n try {\n const p = params as {\n type?: string;\n priority?: number;\n payload?: Record<string, unknown>;\n maxAttempts?: number;\n };\n\n if (!p.type) {\n sendResponse(socket, { id, ok: false, error: \"work_queue_enqueue: type is required\" });\n socket.end();\n return;\n }\n\n const item = enqueue({\n type: p.type as import(\"../../daemon/work-queue.js\").WorkItemType,\n priority: p.priority,\n payload: p.payload ?? {},\n maxAttempts: p.maxAttempts,\n });\n\n notifyNewWork();\n\n sendResponse(socket, { id, ok: true, result: { id: item.id, status: item.status } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_stats — return current queue statistics\n if (method === \"work_queue_stats\") {\n sendResponse(socket, { id, ok: true, result: getQueueStats() });\n socket.end();\n return;\n }\n\n // All other methods: PAI tool dispatch\n try {\n const result = await dispatchTool(method, params);\n sendResponse(socket, { id, ok: true, result });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n}\n","/** The dashboard page: one self-contained file (inline SVG + CSS), updated in place with textContent/attributes only. */\nexport const DASHBOARD_HTML = `<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>PAI memory</title>\n<style>\n :root { color-scheme: light dark; --green:#2e9e5b; --amber:#d99a1c; --red:#d64545; --grey:#8a8f98;\n --bg:#f2f3f5; --card:#fff; --fg:#1b1d21; --mute:#6b7280; --track:rgba(128,128,128,.25); --new:#2e9e5b; --old:#d99a1c; --miss:#8a8f98; }\n @media (prefers-color-scheme: dark) { :root { --bg:#111317; --card:#1b1e24; --fg:#e8eaed; --mute:#9aa0a8; } }\n * { box-sizing: border-box; }\n body { font: 15px/1.4 -apple-system, system-ui, sans-serif; margin: 0; padding: 12px; background: var(--bg); color: var(--fg); }\n main { max-width: 1000px; margin: 0 auto; }\n header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }\n h1 { font-size: 20px; margin: 0; }\n .pill { color: #fff; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; padding: 4px 12px; border-radius: 999px; background: var(--grey); }\n .pill.green { background: var(--green); } .pill.amber { background: var(--amber); } .pill.red { background: var(--red); }\n #reason { color: var(--mute); flex: 1 1 200px; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n .wide { grid-column: 1 / -1; }\n @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }\n .card { background: var(--card); border-radius: 12px; padding: 12px 14px; border-top: 4px solid var(--grey); box-shadow: 0 1px 3px rgba(0,0,0,.12); min-width: 0; }\n .card.green { border-color: var(--green); } .card.amber { border-color: var(--amber); } .card.red { border-color: var(--red); }\n h2 { font-size: 12px; margin: 0 0 8px; text-transform: uppercase; letter-spacing: .06em; color: var(--mute); }\n .k { color: var(--mute); } .bad { color: var(--red); font-weight: 600; }\n .big { font-size: 34px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }\n .unit { font-size: 14px; font-weight: 400; color: var(--mute); }\n .job { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }\n .ring { width: 150px; height: 150px; flex: none; }\n .ring .bg { stroke: var(--track); } .ring .fg { stroke: var(--green); transition: stroke-dashoffset .6s; }\n .ring text { fill: var(--fg); text-anchor: middle; }\n .stats { flex: 1 1 200px; min-width: 0; display: grid; gap: 6px; }\n svg { display: block; }\n .spark { width: 100%; height: 56px; }\n .spark polyline { fill: none; stroke: var(--green); stroke-width: 2; vector-effect: non-scaling-stroke; }\n .spark polygon { fill: var(--green); opacity: .15; }\n .stack { width: 100%; height: 22px; border-radius: 6px; }\n .legend { display: grid; gap: 4px; margin: 8px 0; }\n .legend div { display: flex; align-items: center; gap: 8px; }\n .legend i { width: 12px; height: 12px; border-radius: 3px; flex: none; }\n .legend b { margin-left: auto; font-variant-numeric: tabular-nums; }\n .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--grey); display: inline-block; flex: none; }\n .dot.green { background: var(--green); } .dot.amber { background: var(--amber); } .dot.red { background: var(--red); }\n .line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n .gauge { width: 100%; height: 14px; border-radius: 7px; }\n .pass { padding: 6px 0; border-top: 1px solid var(--track); }\n .pass:first-of-type { border-top: 0; }\n .pass .tl { width: 100%; height: 16px; }\n .pass .tl line { stroke: var(--track); stroke-width: 3; stroke-linecap: round; }\n .times { display: flex; justify-content: space-between; gap: 8px; color: var(--mute); font-size: 13px; flex-wrap: wrap; }\n .err { color: var(--red); font-size: 13px; word-break: break-word; }\n .kv { display: flex; flex-wrap: wrap; gap: 4px 18px; }\n .kv span span { color: var(--mute); margin-right: 4px; }\n #foot { color: var(--mute); font-size: 12px; margin-top: 12px; }\n [hidden] { display: none !important; }\n</style></head><body><main>\n<header><h1>PAI memory</h1><span class=\"pill\" id=\"pill\">…</span><span id=\"reason\">loading…</span></header>\n<div class=\"grid\">\n <section class=\"card wide\" id=\"jobs-empty\"><h2>Re-embed job</h2><span class=\"k\">no job running</span></section>\n <div class=\"wide\" id=\"jobs\" style=\"display:contents\"></div>\n\n <section class=\"card\" id=\"index\"><h2>Index</h2>\n <div class=\"line\"><span class=\"big\" id=\"ix-cov\">?</span><span class=\"unit\" id=\"ix-chunks\"></span></div>\n <svg class=\"stack\" id=\"ix-stack\" viewBox=\"0 0 100 10\" preserveAspectRatio=\"none\" role=\"img\" aria-label=\"vector distribution\">\n <rect id=\"sg-new\" x=\"0\" y=\"0\" width=\"0\" height=\"10\" fill=\"var(--new)\"/>\n <rect id=\"sg-old\" x=\"0\" y=\"0\" width=\"0\" height=\"10\" fill=\"var(--old)\"/>\n <rect id=\"sg-miss\" x=\"0\" y=\"0\" width=\"0\" height=\"10\" fill=\"var(--miss)\"/>\n </svg>\n <div class=\"legend\">\n <div id=\"lg-new\"><i style=\"background:var(--new)\"></i><span>new backend</span><b id=\"n-new\">0</b></div>\n <div id=\"lg-old\"><i style=\"background:var(--old)\"></i><span id=\"t-old\">embedded</span><b id=\"n-old\">0</b></div>\n <div><i style=\"background:var(--miss)\"></i><span>missing</span><b id=\"n-miss\">0</b></div>\n </div>\n <div><span class=\"k\">files</span> <span id=\"ix-files\">?</span></div>\n <div><span class=\"k\">bound to</span> <span id=\"ix-bound\">?</span>, <span class=\"k\">configured</span> <span id=\"ix-conf\">?</span></div>\n <div class=\"bad\" id=\"ix-hint\" hidden></div>\n </section>\n\n <section class=\"card\" id=\"backend\"><h2>Backend</h2>\n <div class=\"line\"><span class=\"dot\" id=\"be-dot\"></span><b id=\"be-id\">?</b><span id=\"be-state\" class=\"k\"></span></div>\n <div class=\"k\" id=\"be-reason\"></div>\n <div style=\"margin-top:10px\"><span class=\"k\">loaded model</span> <b id=\"be-model\">none</b></div>\n <div class=\"line\" style=\"margin-top:8px\"><span class=\"big\" id=\"gpu-pct\">?</span><span class=\"unit\">GPU</span></div>\n <svg class=\"gauge\" viewBox=\"0 0 100 10\" preserveAspectRatio=\"none\" role=\"img\" aria-label=\"GPU share\">\n <rect x=\"0\" y=\"0\" width=\"100\" height=\"10\" fill=\"var(--track)\"/>\n <rect id=\"gpu-bar\" x=\"0\" y=\"0\" width=\"0\" height=\"10\" fill=\"var(--green)\"/>\n </svg>\n </section>\n\n <section class=\"card\" id=\"passes\"><h2>Passes</h2><div id=\"pass-list\"></div></section>\n\n <section class=\"card\" id=\"daemon\"><h2>Daemon</h2>\n <div class=\"kv\">\n <span><span>pid</span><b id=\"d-pid\">?</b></span><span><span>uptime</span><b id=\"d-up\">?</b></span>\n <span><span>version</span><b id=\"d-ver\">?</b></span><span><span>storage</span><b id=\"d-sto\">?</b></span>\n </div>\n </section>\n</div>\n<div id=\"foot\"></div>\n\n<template id=\"job-tpl\"><section class=\"card\">\n <h2></h2>\n <div class=\"job\">\n <svg class=\"ring\" viewBox=\"0 0 120 120\" role=\"img\" aria-label=\"progress\">\n <circle class=\"bg\" cx=\"60\" cy=\"60\" r=\"52\" fill=\"none\" stroke-width=\"12\"/>\n <circle class=\"fg\" cx=\"60\" cy=\"60\" r=\"52\" fill=\"none\" stroke-width=\"12\" stroke-linecap=\"round\" transform=\"rotate(-90 60 60)\" stroke-dasharray=\"326.73\" stroke-dashoffset=\"326.73\"/>\n <text class=\"pct\" x=\"60\" y=\"68\" font-size=\"24\" font-weight=\"700\">0%</text>\n </svg>\n <div class=\"stats\">\n <div><span class=\"big rate\">?</span> <span class=\"unit\">chunks/s</span></div>\n <div><span class=\"k\">done</span> <b class=\"done\">?</b></div>\n <div><span class=\"k\">ETA</span> <b class=\"eta\">?</b> <span class=\"k fin\"></span></div>\n <div class=\"k phase\"></div>\n <div class=\"bad stall\" hidden>STALLED</div>\n </div>\n </div>\n <div class=\"k\" style=\"margin-top:8px;font-size:12px\">rate, last hour</div>\n <svg class=\"spark\" viewBox=\"0 0 240 56\" preserveAspectRatio=\"none\" role=\"img\" aria-label=\"rate sparkline\">\n <polygon points=\"\"/><polyline points=\"\"/>\n </svg>\n</section></template>\n\n<script>\nconst $ = (id) => document.getElementById(id);\nconst n = (x) => (x === null || x === undefined ? \"?\" : Number(x).toLocaleString());\nconst when = (t) => (t ? new Date(t).toLocaleString([], { dateStyle: \"short\", timeStyle: \"short\" }) : \"never\");\nconst clock = (t) => new Date(t).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\nfunction dur(secs) {\n if (secs === null || secs === undefined) return \"?\";\n const h = Math.floor(secs / 3600), m = Math.floor((secs % 3600) / 60);\n return h >= 24 ? Math.floor(h / 24) + \"d \" + (h % 24) + \"h\" : h > 0 ? h + \"h \" + m + \"m\" : m + \"m\";\n}\nconst set = (id, text) => { const e = $(id); if (e.textContent !== text) e.textContent = text; };\nconst lvl = (e, base, level) => { e.className = base + \" \" + (level || \"\"); };\nconst CIRC = 326.73;\n\nfunction sparkPoints(history) {\n const r = [];\n for (let i = 1; i < history.length; i++) {\n const dt = (history[i].t - history[i - 1].t) / 1000;\n r.push(dt > 0 ? Math.max(0, (history[i].done - history[i - 1].done) / dt) : 0);\n }\n if (r.length < 2) return [\"\", \"\"];\n const max = Math.max(...r, 0.001);\n const pts = r.map((v, i) => (i / (r.length - 1) * 240).toFixed(1) + \",\" + (52 - (v / max) * 48).toFixed(1));\n return [pts.join(\" \"), \"0,56 \" + pts.join(\" \") + \" 240,56\"];\n}\n\nconst jobEls = new Map();\nfunction renderJobs(s) {\n const names = new Set(s.jobs.map((j) => j.name));\n for (const [name, e] of jobEls) if (!names.has(name)) { e.remove(); jobEls.delete(name); }\n $(\"jobs-empty\").hidden = s.jobs.length > 0;\n for (const j of s.jobs) {\n let e = jobEls.get(j.name);\n if (!e) { e = $(\"job-tpl\").content.firstElementChild.cloneNode(true); $(\"jobs\").append(e); jobEls.set(j.name, e); }\n const q = (c) => e.querySelector(c);\n const pct = j.total > 0 ? Math.min(100, (j.done / j.total) * 100) : 0;\n e.className = \"card wide \" + j.level;\n q(\"h2\").textContent = \"Re-embed job \" + j.name;\n q(\".fg\").setAttribute(\"stroke-dashoffset\", String(CIRC * (1 - pct / 100)));\n q(\".pct\").textContent = pct.toFixed(1) + \"%\";\n q(\".rate\").textContent = j.rate === null ? \"?\" : j.rate.toFixed(1);\n q(\".done\").textContent = n(j.done) + \" / \" + n(j.total);\n q(\".eta\").textContent = j.etaSecs === null ? \"?\" : dur(j.etaSecs);\n q(\".fin\").textContent = j.etaSecs === null ? \"\" : \"done about \" + clock(s.generatedAt + j.etaSecs * 1000);\n q(\".phase\").textContent = \"phase \" + j.phase + \", last progress \" + when(j.lastProgressAt);\n q(\".stall\").hidden = !j.stalled;\n const [line, area] = sparkPoints(j.history || []);\n q(\"polyline\").setAttribute(\"points\", line);\n q(\"polygon\").setAttribute(\"points\", area);\n }\n}\n\nfunction renderIndex(s) {\n const i = s.index, g = i.segments;\n $(\"index\").className = \"card \" + i.level;\n set(\"ix-cov\", i.coverage === null ? \"?\" : (i.coverage * 100).toFixed(1) + \"%\");\n set(\"ix-chunks\", \"embedded of \" + n(i.chunks) + (i.countsExact ? \"\" : \" (est.)\") + \" chunks\");\n set(\"ix-files\", n(i.files));\n const total = g ? g.fresh + g.old + g.missing : 0;\n const w = (v) => (total > 0 ? (v / total) * 100 : 0);\n const a = w(g ? g.fresh : 0), b = w(g ? g.old : 0), c = w(g ? g.missing : 0);\n const setRect = (id, x, width) => { $(id).setAttribute(\"x\", String(x)); $(id).setAttribute(\"width\", String(width)); };\n setRect(\"sg-new\", 0, a); setRect(\"sg-old\", a, b); setRect(\"sg-miss\", a + b, c);\n const reembed = !!g && s.jobs.some((j) => j.phase !== \"done\") && g.fresh > 0;\n $(\"lg-new\").hidden = !reembed;\n set(\"t-old\", reembed ? \"old backend, still to replace\" : \"embedded\");\n set(\"n-new\", n(g && g.fresh)); set(\"n-old\", n(g && g.old)); set(\"n-miss\", n(g && g.missing));\n const bound = i.binding ? i.binding.backend + (i.binding.model ? \" / \" + i.binding.model : \"\") : \"none\";\n set(\"ix-bound\", bound);\n set(\"ix-conf\", i.configured.backend + \" / \" + i.configured.model);\n const msg = i.error ? \"binding error: \" + i.error : i.hint || \"\";\n $(\"ix-hint\").hidden = !msg; set(\"ix-hint\", msg);\n $(\"ix-hint\").style.color = i.level === \"amber\" ? \"var(--amber)\" : \"\";\n}\n\nfunction renderBackend(s) {\n const b = s.backend, m = b.loaded && b.loaded[0];\n $(\"backend\").className = \"card \" + b.level;\n lvl($(\"be-dot\"), \"dot\", b.available === null ? \"\" : b.available ? \"green\" : \"red\");\n set(\"be-id\", b.id);\n set(\"be-state\", b.available === null ? \"unknown\" : b.available ? \"available\" : \"unavailable\");\n set(\"be-reason\", b.reason || \"\");\n set(\"be-model\", m ? m.name : b.model + \" (not loaded)\");\n const share = m && m.gpuShare !== null ? m.gpuShare : null;\n set(\"gpu-pct\", share === null ? \"?\" : (share * 100).toFixed(0) + \"%\");\n $(\"gpu-bar\").setAttribute(\"width\", String(share === null ? 0 : Math.min(100, share * 100)));\n $(\"gpu-bar\").setAttribute(\"fill\", share !== null && share < 1 ? \"var(--amber)\" : \"var(--green)\");\n}\n\nconst passEls = new Map();\nconst NS = \"http://www.w3.org/2000/svg\";\nfunction passRow(name) {\n const e = document.createElement(\"div\"); e.className = \"pass\";\n e.innerHTML = '<div class=\"line\"><span class=\"dot\"></span><b></b><span class=\"k st\"></span></div>' +\n '<svg class=\"tl\" viewBox=\"0 0 100 16\" preserveAspectRatio=\"none\"><line x1=\"4\" y1=\"8\" x2=\"96\" y2=\"8\"/></svg>' +\n '<div class=\"times\"><span class=\"last\"></span><span class=\"next\"></span></div><div class=\"err\" hidden></div>';\n e.querySelector(\"b\").textContent = name;\n const svg = e.querySelector(\"svg\");\n const mk = (cls, cx) => { const c = document.createElementNS(NS, \"ellipse\"); c.setAttribute(\"class\", cls); c.setAttribute(\"cx\", cx); c.setAttribute(\"cy\", \"8\"); c.setAttribute(\"rx\", \"2.2\"); c.setAttribute(\"ry\", \"5\"); svg.append(c); return c; };\n mk(\"m-last\", \"4\"); mk(\"m-next\", \"96\");\n return e;\n}\nfunction renderPasses(s) {\n $(\"passes\").className = \"card \" + (s.passes.some((p) => p.level === \"red\") ? \"red\" : \"green\");\n for (const p of s.passes) {\n let e = passEls.get(p.name);\n if (!e) { e = passRow(p.name); $(\"pass-list\").append(e); passEls.set(p.name, e); }\n const q = (c) => e.querySelector(c);\n const state = p.failure ? \"red\" : p.running ? \"amber\" : \"green\";\n lvl(q(\".dot\"), \"dot\", state);\n q(\".st\").textContent = p.failure ? \"failed\" : p.running ? \"running\" : \"ok\";\n q(\".last\").textContent = \"last \" + (p.lastEnd ? when(p.lastEnd) : \"none since start\");\n q(\".next\").textContent = p.nextAt ? \"next \" + when(p.nextAt) : \"\";\n const col = \"var(--\" + state + \")\";\n q(\".m-last\").setAttribute(\"fill\", col);\n q(\".m-next\").setAttribute(\"fill\", \"none\"); q(\".m-next\").setAttribute(\"stroke\", \"var(--grey)\"); q(\".m-next\").setAttribute(\"stroke-width\", \"1.5\");\n q(\".m-next\").style.display = p.nextAt ? \"\" : \"none\";\n const err = q(\".err\");\n err.hidden = !p.failure;\n if (p.failure) err.textContent = p.failure.error + \" (\" + (p.failure.gaveUp ? \"gave up until next scheduled run\" : \"retry \" + when(p.failure.retryAt)) + \")\";\n }\n}\n\nfunction render(s) {\n lvl($(\"pill\"), \"pill\", s.level);\n set(\"pill\", s.level === \"green\" ? \"healthy\" : s.level === \"amber\" ? \"attention\" : \"problem\");\n set(\"reason\", s.reason);\n renderJobs(s); renderIndex(s); renderBackend(s); renderPasses(s);\n set(\"d-pid\", String(s.daemon.pid)); set(\"d-up\", dur(s.daemon.uptimeSecs));\n set(\"d-ver\", s.daemon.version); set(\"d-sto\", s.daemon.storage);\n set(\"foot\", \"updated \" + new Date(s.generatedAt).toLocaleTimeString() + \" · refreshes every 10 s\");\n}\nasync function load() {\n try {\n const r = await fetch(\"/api/status\", { cache: \"no-store\" });\n if (!r.ok) throw new Error(\"HTTP \" + r.status);\n render(await r.json());\n } catch (e) {\n lvl($(\"pill\"), \"pill\", \"red\"); set(\"pill\", \"unreachable\");\n set(\"reason\", \"daemon unreachable: \" + e.message);\n }\n}\nload();\nsetInterval(load, 10000);\n</script></main></body></html>\n`;\n","/**\n * dashboard.ts — read-only memory status dashboard served by the daemon.\n *\n * Embedding and index passes run unattended for hours; a crashed embed pass\n * went unnoticed for 7 h. This puts what is running and whether it is healthy\n * on one page (and /api/status) reachable from a phone on the tailnet.\n *\n * Nothing here computes anything per request: a background tick (30 s) samples\n * the job table, binding and backend health, and real row counts are refreshed\n * at most every 5 min. Requests only assemble the cached values.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { createServer, type Server } from \"node:http\";\nimport { promisify } from \"node:util\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { EmbeddingBinding, StorageBackend } from \"../storage/interface.js\";\nimport type { EmbeddingBackend } from \"../memory/backends/types.js\";\nimport { bindingMatches, mismatchReason } from \"../memory/embedding-binding.js\";\nimport { DASHBOARD_HTML } from \"./dashboard-page.js\";\nimport type { DashboardConfig } from \"./config.js\";\nimport { paiHomePath } from \"../config/pai-home.js\";\n\nconst TICK_MS = 30_000;\nconst COUNTS_TTL_MS = 5 * 60_000;\nconst STALL_MS = 5 * 60_000;\nconst RATE_WINDOW_MS = 5 * 60_000;\nconst HISTORY_MAX = 120; // 1 h of 30 s samples\nconst HISTORY_PERSIST_MS = 30_000; // write at most every 30s\nconst HISTORY_PRUNE_AGE_MS = 2 * 60 * 60_000; // 2 hours\n\nexport type Level = \"green\" | \"amber\" | \"red\";\nconst RANK: Record<Level, number> = { green: 0, amber: 1, red: 2 };\nconst worst = (ls: Level[]): Level => ls.reduce((a, b) => (RANK[b] > RANK[a] ? b : a), \"green\" as Level);\n\n// ---------------------------------------------------------------------------\n// Host check (DNS-rebinding guard)\n// ---------------------------------------------------------------------------\n\n/** Host header without port; IPv6 literals keep their brackets stripped. */\nfunction hostName(header: string | undefined): string {\n if (!header) return \"\";\n const h = header.trim().toLowerCase();\n if (h.startsWith(\"[\")) return h.slice(1, h.indexOf(\"]\"));\n return h.replace(/:\\d+$/, \"\");\n}\n\nexport function hostAllowed(header: string | undefined, allowed: Iterable<string>): boolean {\n const h = hostName(header);\n return h !== \"\" && new Set([...allowed].map((a) => a.toLowerCase())).has(h);\n}\n\n/** Tailnet DNS name and IPs of this machine from `tailscale status --json`; empty when unavailable. */\nexport async function tailnetHosts(): Promise<string[]> {\n try {\n const { stdout } = await promisify(execFile)(\"tailscale\", [\"status\", \"--json\"], { timeout: 3000 });\n const self = (JSON.parse(stdout) as { Self?: { DNSName?: string; TailscaleIPs?: string[] } }).Self;\n return [self?.DNSName?.replace(/\\.$/, \"\"), ...(self?.TailscaleIPs ?? [])].filter((x): x is string => !!x);\n } catch {\n return [];\n }\n}\n\n// ---------------------------------------------------------------------------\n// Sources (everything the dashboard reads; faked in tests)\n// ---------------------------------------------------------------------------\n\nexport interface Counts {\n files: number;\n chunks: number;\n /** null when not known (SQLite estimate). */\n embedded: number | null;\n}\n\nexport interface RawJob {\n name: string;\n state: Record<string, unknown>;\n}\n\nexport interface LoadedModel {\n name: string;\n size: number;\n sizeVram: number;\n}\n\nexport interface PassSnapshot {\n name: string;\n running: boolean;\n /** Epoch ms of the last successful pass end, 0 = none since start. */\n lastEnd: number;\n failure: { at: number; error: string; attempts: number; gaveUp: boolean; retryAt: number | null } | null;\n nextAt: number | null;\n}\n\nexport interface DashboardSource {\n storage: string;\n daemon(): { pid: number; startTime: number; version: string };\n /** Cheap (catalog statistics); used at start and between real refreshes. */\n estimate(): Promise<Counts>;\n /** Real counts; may be slow, must be bounded by the implementation. */\n exact(): Promise<Counts>;\n binding(): Promise<EmbeddingBinding | null>;\n jobs(): Promise<RawJob[]>;\n backend(): EmbeddingBackend;\n loadedModels(): Promise<LoadedModel[] | null>;\n passes(): PassSnapshot[];\n}\n\n// ---------------------------------------------------------------------------\n// Cached state: tick() samples, status() assembles\n// ---------------------------------------------------------------------------\n\ninterface JobSample {\n t: number;\n done: number;\n}\n\ninterface PersistedHistory {\n jobs: Record<string, JobSample[]>;\n}\n\ninterface Cache {\n counts: (Counts & { exact: boolean; at: number }) | null;\n binding: EmbeddingBinding | null;\n bindingError: string | null;\n jobs: RawJob[];\n backend: { ok: boolean; reason: string } | null;\n loaded: LoadedModel[] | null;\n}\n\nexport interface JobView {\n name: string;\n phase: \"prepare\" | \"carry\" | \"embed\" | \"finish\" | \"done\";\n done: number;\n total: number;\n /** Rows per second over the last minutes; null until two samples exist. */\n rate: number | null;\n etaSecs: number | null;\n lastProgressAt: number;\n stalled: boolean;\n level: Level;\n /** Last hour of samples (oldest first); the page derives the per-interval rate from it. */\n history: JobSample[];\n}\n\nexport interface IndexSegments {\n /** Vectors from the new backend (re-embed done count), 0 when no re-embed runs. */\n fresh: number;\n /** Embedded by the old backend, still to replace. */\n old: number;\n missing: number;\n}\n\n/** Stacked-bar numbers: done/embedded/total in, three non-negative segments out. `done` null = no re-embed. */\nexport function indexSegments(total: number, embedded: number, done: number | null): IndexSegments {\n const missing = Math.max(0, total - embedded);\n if (done === null) return { fresh: 0, old: Math.max(0, embedded), missing };\n return { fresh: done, old: Math.max(0, embedded - done), missing };\n}\n\nfunction phaseOf(s: Record<string, unknown>): JobView[\"phase\"] {\n if (s.finished) return \"done\";\n if (s.embedDone) return \"finish\";\n if (s.carryDone) return \"embed\";\n if (s.prepared) return \"carry\";\n return \"prepare\";\n}\n\nexport class DashboardState {\n private cache: Cache = { counts: null, binding: null, bindingError: null, jobs: [], backend: null, loaded: null };\n private samples = new Map<string, { list: JobSample[]; history: JobSample[]; progressAt: number; lastDone: number }>();\n private lastExactAt: number;\n private exactRunning = false;\n private lastPersistAt: number;\n private historyPath: string;\n\n constructor(private src: DashboardSource, private now: () => number = Date.now, historyPath?: string) {\n this.lastExactAt = now();\n this.lastPersistAt = now();\n this.historyPath = historyPath ?? paiHomePath(\"state\", \"dashboard-history.json\");\n }\n\n async loadHistory(): Promise<void> {\n try {\n if (!existsSync(this.historyPath)) return;\n const content = await readFile(this.historyPath, \"utf8\");\n const data = JSON.parse(content) as PersistedHistory;\n const now = this.now();\n for (const [name, hist] of Object.entries(data.jobs ?? {})) {\n const pruned = hist.filter((s) => now - s.t <= HISTORY_PRUNE_AGE_MS);\n if (pruned.length > 0) {\n const s = this.samples.get(name) ?? { list: [], history: [], progressAt: now, lastDone: 0 };\n s.history = pruned.slice(-HISTORY_MAX);\n this.samples.set(name, s);\n }\n }\n } catch {\n // silently ignore load errors\n }\n }\n\n private async persistHistory(): Promise<void> {\n const t = this.now();\n if (t - this.lastPersistAt < HISTORY_PERSIST_MS) return;\n this.lastPersistAt = t;\n\n try {\n const jobs: Record<string, JobSample[]> = {};\n for (const [name, s] of this.samples) {\n if (s.history.length > 0) jobs[name] = s.history;\n }\n const data: PersistedHistory = { jobs };\n const dir = join(this.historyPath, \"..\");\n await mkdir(dir, { recursive: true });\n const tmp = this.historyPath + \".tmp\";\n await writeFile(tmp, JSON.stringify(data), \"utf8\");\n await writeFile(this.historyPath, JSON.stringify(data), \"utf8\");\n } catch {\n // silently ignore persist errors\n }\n }\n\n /** Sample everything once. Never throws: a failing source shows up in the status instead. */\n async tick(): Promise<void> {\n const t = this.now();\n const c = this.cache;\n const run = async <T>(f: () => Promise<T>, onErr: (e: unknown) => void): Promise<T | undefined> => {\n try { return await f(); } catch (e) { onErr(e); return undefined; }\n };\n\n if (!c.counts) {\n const est = await run(() => this.src.estimate(), () => {});\n if (est) c.counts = { ...est, exact: false, at: t };\n }\n this.refreshExact(t);\n\n c.bindingError = null;\n const b = await run(() => this.src.binding(), (e) => { c.bindingError = e instanceof Error ? e.message : String(e); });\n if (b !== undefined) c.binding = b;\n\n const jobs = await run(() => this.src.jobs(), () => {});\n if (jobs) {\n c.jobs = jobs;\n for (const j of jobs) this.sample(j, t);\n }\n\n const be = this.src.backend();\n c.backend = (await run(() => be.available(), () => {})) ?? { ok: false, reason: \"availability probe threw\" };\n c.loaded = be.id.startsWith(\"ollama\") ? (await run(() => this.src.loadedModels(), () => {})) ?? null : null;\n\n await this.persistHistory();\n }\n\n /** Real counts at most every COUNTS_TTL_MS, in the background; the tick never waits for them. */\n private refreshExact(t: number): void {\n if (this.exactRunning || t - this.lastExactAt < COUNTS_TTL_MS) return;\n this.exactRunning = true;\n this.lastExactAt = t;\n this.src.exact().then(\n (n) => { this.cache.counts = { ...n, exact: true, at: this.now() }; },\n () => {},\n ).finally(() => { this.exactRunning = false; });\n }\n\n private sample(j: RawJob, t: number): void {\n const done = Number(j.state.done ?? 0);\n const s = this.samples.get(j.name) ?? { list: [], history: [], progressAt: t, lastDone: done };\n if (done !== s.lastDone) { s.progressAt = t; s.lastDone = done; }\n s.list.push({ t, done });\n s.history.push({ t, done });\n if (s.history.length > HISTORY_MAX) s.history.shift();\n while (s.list.length > 2 && t - s.list[0].t > RATE_WINDOW_MS) s.list.shift();\n this.samples.set(j.name, s);\n }\n\n private jobView(j: RawJob, t: number): JobView {\n const total = Number(j.state.total ?? 0);\n const done = Number(j.state.done ?? 0);\n const phase = phaseOf(j.state);\n const s = this.samples.get(j.name);\n const first = s?.list[0];\n const last = s?.list[s.list.length - 1];\n const rate = first && last && last.t > first.t ? Math.max(0, (last.done - first.done) / ((last.t - first.t) / 1000)) : null;\n const etaSecs = rate && rate > 0 && total > done ? Math.round((total - done) / rate) : null;\n // Progress time = when `done` last changed as seen by this daemon; first sight if never.\n const lastProgressAt = s?.progressAt ?? t;\n const stalled = phase !== \"done\" && t - lastProgressAt >= STALL_MS;\n return { name: j.name, phase, done, total, rate, etaSecs, lastProgressAt, stalled, level: stalled ? \"red\" : \"green\", history: s?.history ?? [] };\n }\n\n status() {\n const t = this.now();\n const c = this.cache;\n const d = this.src.daemon();\n const be = this.src.backend();\n\n const jobs = c.jobs.filter((j) => Object.keys(j.state).length > 0).map((j) => this.jobView(j, t));\n\n const mismatch = !!c.binding && !bindingMatches(c.binding, be);\n const counts = c.counts;\n\n // Check for active re-embed job for the configured backend\n const reembedJob = this.findReembedJob(c.jobs, be.id);\n const reembedActive = reembedJob && ![\"done\"].includes(phaseOf(reembedJob.state));\n\n const indexLevel: Level = (mismatch && !reembedActive) || c.bindingError ? \"red\" : reembedActive ? \"amber\" : \"green\";\n const backendLevel: Level = c.backend && !c.backend.ok ? \"red\" : c.backend ? \"green\" : \"amber\";\n\n const passes = this.src.passes().map((p) => ({\n ...p,\n level: (p.failure ? \"red\" : \"green\") as Level,\n }));\n\n let hint: string | null = null;\n if (mismatch && c.binding) {\n if (reembedActive) {\n const jobBackend = this.deriveBackendFromJobName(reembedJob!.name);\n hint = `re-embed to ${jobBackend} in progress, semantic search keyword-only until it finishes`;\n } else {\n hint = mismatchReason(c.binding, be);\n }\n }\n\n const level = worst([indexLevel, backendLevel, ...jobs.map((j) => j.level), ...passes.map((p) => p.level)]);\n const reason =\n level === \"green\" ? \"all systems healthy\"\n : c.bindingError ? `binding check failed: ${c.bindingError}`\n : jobs.find((j) => j.stalled) ? `job ${jobs.find((j) => j.stalled)!.name} stalled`\n : c.backend && !c.backend.ok ? `backend unavailable: ${c.backend.reason}`\n : passes.find((p) => p.failure) ? `pass ${passes.find((p) => p.failure)!.name} failed`\n : hint ?? (reembedActive ? `re-embed ${reembedJob!.name} running` : c.backend ? \"degraded\" : \"backend not probed yet\");\n const activeJob = reembedActive ? jobs.find((j) => j.name === reembedJob!.name) : undefined;\n const segments = counts && counts.embedded !== null ? indexSegments(counts.chunks, counts.embedded, activeJob ? activeJob.done : null) : null;\n\n return {\n generatedAt: t,\n level,\n reason,\n daemon: { pid: d.pid, uptimeSecs: Math.floor((t - d.startTime) / 1000), version: d.version, storage: this.src.storage },\n index: {\n level: indexLevel,\n files: counts?.files ?? null,\n chunks: counts?.chunks ?? null,\n embedded: counts?.embedded ?? null,\n coverage: counts && counts.embedded !== null && counts.chunks > 0 ? Math.min(1, counts.embedded / counts.chunks) : null,\n countsExact: counts?.exact ?? false,\n countsAt: counts?.at ?? null,\n segments,\n binding: c.binding,\n configured: { backend: be.id, model: be.model, dims: be.dims },\n mismatch,\n hint,\n error: c.bindingError,\n },\n jobs,\n passes,\n backend: {\n level: backendLevel,\n id: be.id,\n model: be.model,\n available: c.backend?.ok ?? null,\n reason: c.backend?.reason ?? null,\n loaded: c.loaded?.map((m) => ({ name: m.name, size: m.size, sizeVram: m.sizeVram, gpuShare: m.size > 0 ? m.sizeVram / m.size : null })) ?? null,\n },\n };\n }\n\n private findReembedJob(jobs: RawJob[], configuredBackendId: string): RawJob | undefined {\n return jobs.find((j) => j.name.endsWith(\"-reembed\") && this.deriveBackendFromJobName(j.name) === configuredBackendId);\n }\n\n private deriveBackendFromJobName(jobName: string): string {\n const match = jobName.match(/^(.+)-reembed$/);\n return match ? match[1] : jobName;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP server\n// ---------------------------------------------------------------------------\n\nexport interface DashboardHandle {\n server: Server;\n url: string;\n stop(): void;\n}\n\nexport async function startDashboard(\n cfg: DashboardConfig,\n src: DashboardSource,\n opts: { extraHosts?: string[]; historyPath?: string } = {},\n): Promise<DashboardHandle> {\n const state = new DashboardState(src, Date.now, opts.historyPath);\n await state.loadHistory();\n const allowed = [cfg.bind, \"localhost\", \"127.0.0.1\", \"::1\", ...(opts.extraHosts ?? (await tailnetHosts()))];\n\n const server = createServer((req, res) => {\n const deny = (code: number, msg: string) => { res.writeHead(code, { \"content-type\": \"text/plain\" }); res.end(msg); };\n if (!hostAllowed(req.headers.host, allowed)) return deny(403, \"forbidden host\");\n if (req.method !== \"GET\" && req.method !== \"HEAD\") return deny(405, \"read-only\");\n const path = (req.url ?? \"/\").split(\"?\")[0];\n if (path === \"/api/status\") {\n res.writeHead(200, { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" });\n return res.end(JSON.stringify(state.status()));\n }\n if (path === \"/\") {\n res.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\", \"cache-control\": \"no-store\" });\n return res.end(DASHBOARD_HTML);\n }\n deny(404, \"not found\");\n });\n\n await state.tick();\n const timer = setInterval(() => void state.tick(), TICK_MS);\n timer.unref();\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(cfg.port, cfg.bind, resolve);\n });\n return {\n server,\n url: `http://${cfg.bind}:${cfg.port}/`,\n stop: () => { clearInterval(timer); server.close(); },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Live source (Postgres or SQLite storage + configured backend)\n// ---------------------------------------------------------------------------\n\nasync function ollamaPs(baseUrl: string): Promise<LoadedModel[] | null> {\n try {\n const r = await fetch(`${baseUrl.replace(/\\/+$/, \"\")}/api/ps`, { signal: AbortSignal.timeout(2000) });\n if (!r.ok) return null;\n const j = (await r.json()) as { models?: Array<{ name: string; size: number; size_vram: number }> };\n return (j.models ?? []).map((m) => ({ name: m.name, size: m.size, sizeVram: m.size_vram }));\n } catch {\n return null;\n }\n}\n\nexport function liveSource(deps: {\n storage: StorageBackend;\n backend: () => EmbeddingBackend;\n ollamaBaseUrl: string;\n daemon: DashboardSource[\"daemon\"];\n passes: DashboardSource[\"passes\"];\n}): DashboardSource {\n const st = deps.storage;\n const sqliteCounts = async (): Promise<Counts> => ({ ...(await deps.storage.getStats()), embedded: null });\n return {\n storage: deps.storage.backendType,\n daemon: deps.daemon,\n estimate: st.estimateCounts?.bind(st) ?? sqliteCounts,\n exact: st.exactCounts?.bind(st) ?? sqliteCounts,\n binding: () => deps.storage.getEmbeddingBinding(),\n jobs: st.embeddingJobs?.bind(st) ?? (async () => []),\n backend: deps.backend,\n loadedModels: () => ollamaPs(deps.ollamaBaseUrl),\n passes: deps.passes,\n };\n}\n","/**\n * Wires the status dashboard to the daemon's live state (state.ts, scheduler\n * bookkeeping) and the configured embedding backend.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolveFromModule } from \"../../module-paths.js\";\nimport { getConfiguredBackend } from \"../../memory/backends/index.js\";\nimport { liveSource, startDashboard, type DashboardHandle, type PassSnapshot } from \"../dashboard.js\";\nimport type { PaiDaemonConfig } from \"../config.js\";\nimport {\n daemonConfig,\n embedInProgress,\n indexInProgress,\n lastEmbedTime,\n lastIndexTime,\n lastVaultIndexTime,\n passFailures,\n startTime,\n vaultIndexInProgress,\n storageBackend,\n} from \"./state.js\";\nimport { PASS_RETRY_DELAYS_MS, msUntilNextAnchor } from \"./scheduler.js\";\n\nexport function packageVersion(): string {\n try {\n return (JSON.parse(readFileSync(resolveFromModule(import.meta.url, \"package.json\"), \"utf8\")) as { version?: string }).version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n/** The index/embed/vault passes as the dashboard shows them, from the existing daemon state. */\nexport function livePasses(now = Date.now()): PassSnapshot[] {\n const cfg = daemonConfig;\n const next = (last: number, intervalSecs: number): number =>\n cfg.maintenanceHour !== undefined\n ? now + msUntilNextAnchor(cfg.maintenanceHour, intervalSecs * 1000)\n : (last || now) + intervalSecs * 1000;\n const snap = (name: string, running: boolean, lastEnd: number, nextAt: number | null): PassSnapshot => {\n const f = passFailures.get(name);\n const delay = f ? PASS_RETRY_DELAYS_MS[f.attempts - 1] : undefined;\n return {\n name,\n running,\n lastEnd,\n failure: f ? { ...f, retryAt: f.gaveUp || delay === undefined ? null : f.at + delay } : null,\n nextAt,\n };\n };\n return [\n snap(\"index\", indexInProgress, lastIndexTime, next(lastIndexTime, cfg.indexIntervalSecs)),\n snap(\"embed\", embedInProgress, lastEmbedTime, next(lastEmbedTime, cfg.embedIntervalSecs)),\n // The vault pass runs inside the index cycle, so it has no schedule of its own.\n snap(\"vault\", vaultIndexInProgress, lastVaultIndexTime, null),\n ];\n}\n\n/** Start the dashboard when enabled; a failure to bind is logged, never fatal for the daemon. */\nexport async function startLiveDashboard(config: PaiDaemonConfig): Promise<DashboardHandle | null> {\n if (!config.dashboard.enabled) return null;\n try {\n const handle = await startDashboard(\n config.dashboard,\n liveSource({\n storage: storageBackend,\n backend: () => getConfiguredBackend(config),\n ollamaBaseUrl: config.embedding?.ollama?.baseUrl ?? \"http://127.0.0.1:11434\",\n daemon: () => ({ pid: process.pid, startTime, version: packageVersion() }),\n passes: livePasses,\n }),\n );\n process.stderr.write(`[pai-daemon] Dashboard: ${handle.url}\\n`);\n return handle;\n } catch (e) {\n process.stderr.write(`[pai-daemon] Dashboard not started: ${e instanceof Error ? e.message : String(e)}\\n`);\n return null;\n }\n}\n","/**\n * One-time repair for chunks written while the `pai_chunks_fts_update`\n * trigger used the 'english' text-search config instead of 'simple'.\n *\n * The trigger stemmed and stripped stop-words from every stored fts_vector,\n * but every query (searchKeyword, findTunnels) and the direct-insert path\n * (insertChunks) always used 'simple' — so a BEFORE trigger silently\n * overwrote each insert with a vector that keyword search could never match.\n * Measured on a real database: 0 rows for `fts_vector @@ to_tsquery('simple',\n * 'running')` against 142,559 chunks containing \"running\".\n *\n * Completion is marked by a comment on pai_chunks.fts_vector so this can run\n * on every daemon boot for free once migrated, and resume if a batch run\n * was interrupted: every 20 batches the last completed id is stored in the\n * comment as `fts:simple:inprogress:<id>` and the next start continues after it.\n *\n * The rebuild runs in the background while the indexer writes pai_chunks, so\n * each batch UPDATE locks its rows in id order (consistent with other writers)\n * and is retried up to 5 times on deadlock (40P01) or serialization failure\n * (40001) with a 200ms * attempt backoff. Rows whose fts_vector is already\n * correct are skipped, so a retry or restart is cheap.\n */\n\nimport type { StorageBackend } from \"../interface.js\";\nimport type { PostgresBackend } from \"../postgres.js\";\n\nconst DONE_MARKER = \"fts:simple\";\nconst PROGRESS_PREFIX = \"fts:simple:inprogress:\";\nconst BATCH_SIZE = 1000;\nconst MAX_ATTEMPTS = 5;\nconst RETRYABLE_CODES = new Set([\"40P01\", \"40001\"]);\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function migrateFtsConfig(backend: StorageBackend): Promise<void> {\n // Obtained inside the async function body, not by the caller: a throw here\n // (e.g. a bad cast) becomes a rejection of this function's own promise,\n // caught by the caller's `.catch`, instead of a synchronous throw inside\n // the daemon's storage-backend `.then()` that would abort loadQueue()/\n // startWorker() and hit the outer fatal-exit `.catch`.\n const pool = (backend as unknown as PostgresBackend).getPool();\n const tableCheck = await pool.query(\n `SELECT 1 FROM information_schema.tables WHERE table_name = 'pai_chunks'`\n );\n if (tableCheck.rowCount === 0) return;\n\n const commentCheck = await pool.query<{ comment: string | null }>(\n `SELECT col_description('pai_chunks'::regclass, ordinal_position) AS comment\n FROM information_schema.columns\n WHERE table_name = 'pai_chunks' AND column_name = 'fts_vector'`\n );\n const comment = commentCheck.rows[0]?.comment ?? null;\n if (comment === DONE_MARKER) return;\n const resumeId = comment?.startsWith(PROGRESS_PREFIX)\n ? comment.slice(PROGRESS_PREFIX.length)\n : \"\";\n\n process.stderr.write(\n \"[pai-postgres] FTS config migration: starting (english -> simple)\\n\"\n );\n\n const fnCheck = await pool.query(\n `SELECT 1 FROM pg_proc WHERE proname = 'pai_chunks_fts_update'`\n );\n if (fnCheck.rowCount !== 0) {\n await pool.query(`\n CREATE OR REPLACE FUNCTION pai_chunks_fts_update()\n RETURNS TRIGGER AS $$\n BEGIN\n NEW.fts_vector := to_tsvector('simple', COALESCE(NEW.text, ''));\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql;\n `);\n process.stderr.write(\n \"[pai-postgres] FTS config migration: trigger function now uses 'simple'\\n\"\n );\n }\n\n let lastId: string | null = resumeId || null;\n if (lastId !== null) {\n process.stderr.write(`[pai-postgres] FTS config migration: resuming from ${lastId}\\n`);\n }\n let totalUpdated = 0;\n let batchN = 0;\n while (true) {\n const sql = lastId === null\n ? `SELECT id FROM pai_chunks ORDER BY id LIMIT $1`\n : `SELECT id FROM pai_chunks WHERE id > $2 ORDER BY id LIMIT $1`;\n const params: Array<number | string> = lastId === null ? [BATCH_SIZE] : [BATCH_SIZE, lastId];\n const idRows = await pool.query<{ id: string }>(sql, params);\n if (idRows.rows.length === 0) break;\n\n const ids: string[] = idRows.rows.map((r) => r.id);\n for (let attempt = 1; ; attempt++) {\n try {\n const result = await pool.query(\n `UPDATE pai_chunks c SET fts_vector = to_tsvector('simple', COALESCE(c.text, ''))\n FROM (SELECT id FROM pai_chunks WHERE id = ANY($1::text[]) ORDER BY id FOR UPDATE) s\n WHERE c.id = s.id\n AND c.fts_vector IS DISTINCT FROM to_tsvector('simple', COALESCE(c.text, ''))`,\n [ids]\n );\n totalUpdated += result.rowCount ?? 0;\n break;\n } catch (err) {\n const code = (err as { code?: string }).code;\n if (attempt >= MAX_ATTEMPTS || !code || !RETRYABLE_CODES.has(code)) throw err;\n await sleep(200 * attempt);\n }\n }\n lastId = ids[ids.length - 1];\n batchN++;\n if (batchN % 20 === 0) {\n // COMMENT takes no bind params; double quotes to escape the literal.\n await pool.query(\n `COMMENT ON COLUMN pai_chunks.fts_vector IS '${PROGRESS_PREFIX}${lastId.replace(/'/g, \"''\")}'`\n );\n process.stderr.write(\n `[pai-postgres] FTS config migration: ${totalUpdated} chunks rebuilt so far...\\n`\n );\n }\n if (idRows.rows.length < BATCH_SIZE) break;\n }\n\n await pool.query(\n `COMMENT ON COLUMN pai_chunks.fts_vector IS '${DONE_MARKER}'`\n );\n\n process.stderr.write(\n `[pai-postgres] FTS config migration: done, ${totalUpdated} chunks rebuilt\\n`\n );\n}\n","/**\n * IPC server and daemon entry point.\n * Owns: isSocketLive, startIpcServer, serve (exported).\n */\n\nimport { existsSync, unlinkSync } from \"node:fs\";\nimport { createServer, connect, Socket, Server } from \"node:net\";\nimport { setPriority } from \"node:os\";\nimport { createStorageBackend, createRegistryBackend } from \"../../storage/factory.js\";\nimport { configureEmbeddingModel } from \"../../memory/embeddings.js\";\nimport { loadNotificationConfig } from \"../../notifications/config.js\";\nimport { writeDaemonPidfile, removeDaemonPidfile } from \"../daemon-pid.js\";\nimport type { PaiDaemonConfig } from \"../config.js\";\nimport type { IpcRequest } from \"./types.js\";\nimport {\n setRegistryBackend,\n setStorageBackend,\n setDaemonConfig,\n setStartTime,\n setNotificationConfig,\n setShutdownRequested,\n indexInProgress,\n embedInProgress,\n indexSchedulerTimer,\n embedSchedulerTimer,\n cacheKeepaliveTimer,\n storageBackend,\n notificationConfig,\n} from \"./state.js\";\nimport { startIndexScheduler, startEmbedScheduler, startRegistryScanScheduler, startWorkerSupervisor, startCacheKeepalive, startSessionKeepalive } from \"./scheduler.js\";\nimport { handleRequest, sendResponse } from \"./handler.js\";\nimport { loadQueue } from \"../../daemon/work-queue.js\";\nimport { startWorker, stopWorker } from \"../../daemon/work-queue-worker.js\";\nimport { startLiveDashboard } from \"./dashboard-live.js\";\nimport type { DashboardHandle } from \"../dashboard.js\";\nimport { migrateFtsConfig } from \"../../storage/postgres/fts-migration.js\";\n\n// ---------------------------------------------------------------------------\n// IPC helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether an existing socket file is actually being served by a live process.\n */\nfunction isSocketLive(path: string): Promise<boolean> {\n return new Promise((resolve) => {\n const client = connect(path);\n const timer = setTimeout(() => { client.destroy(); resolve(false); }, 500);\n client.on(\"connect\", () => { clearTimeout(timer); client.end(); resolve(true); });\n client.on(\"error\", () => { clearTimeout(timer); resolve(false); });\n });\n}\n\n/**\n * Start the Unix Domain Socket IPC server.\n */\nasync function startIpcServer(socketPath: string): Promise<Server> {\n if (existsSync(socketPath)) {\n const live = await isSocketLive(socketPath);\n if (live) {\n throw new Error(\"Another daemon is already running — socket is live. Aborting startup.\");\n }\n try {\n unlinkSync(socketPath);\n process.stderr.write(\"[pai-daemon] Removed stale socket file.\\n\");\n } catch {\n // If we can't remove it, bind will fail with a clear error\n }\n }\n\n const server = createServer((socket: Socket) => {\n let buffer = \"\";\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n let nl: number;\n while ((nl = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n if (line.trim() === \"\") continue;\n\n let request: IpcRequest;\n try {\n request = JSON.parse(line) as IpcRequest;\n } catch {\n sendResponse(socket, { id: \"?\", ok: false, error: \"Invalid JSON\" });\n socket.destroy();\n return;\n }\n\n handleRequest(request, socket).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id: request.id, ok: false, error: msg });\n socket.destroy();\n });\n }\n });\n\n socket.on(\"error\", () => {\n // Client disconnected — nothing to do\n });\n });\n\n server.on(\"error\", (e) => {\n process.stderr.write(`[pai-daemon] IPC server error: ${e}\\n`);\n });\n\n server.listen(socketPath, () => {\n process.stderr.write(\n `[pai-daemon] IPC server listening on ${socketPath}\\n`\n );\n });\n\n return server;\n}\n\n// ---------------------------------------------------------------------------\n// Main daemon entry point\n// ---------------------------------------------------------------------------\n\nexport async function serve(config: PaiDaemonConfig): Promise<void> {\n setDaemonConfig(config);\n setStartTime(Date.now());\n\n setNotificationConfig(loadNotificationConfig());\n\n process.stderr.write(\"[pai-daemon] Starting daemon...\\n\");\n process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\\n`);\n process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\\n`);\n process.stderr.write(\n `[pai-daemon] Notification mode: ${notificationConfig.mode}\\n`\n );\n\n // Lower scheduling priority so the daemon yields CPU to interactive sessions\n try { setPriority(process.pid, 10); } catch { /* non-fatal */ }\n\n configureEmbeddingModel(config.embeddingModel);\n\n // Connect the registry backend in the background — non-blocking, since it\n // may take a few retries under Postgres (waitForPostgres: true never\n // silently falls back to SQLite, matching the federation backend below).\n // Callers that need it before it resolves (status, observation IPC methods)\n // already tolerate an unready backend via their own try/catch.\n void createRegistryBackend(config, { waitForPostgres: true })\n .then((backend) => {\n setRegistryBackend(backend);\n process.stderr.write(`[pai-daemon] Registry backend: ${backend.backendType}\\n`);\n })\n .catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\\n`);\n process.exit(1);\n });\n\n // Start the IPC server immediately so `pai daemon status` answers even while\n // we are still waiting for the federation backend (e.g. Postgres coming up\n // after a reboot). The status handler tolerates a not-yet-ready backend.\n const server = await startIpcServer(config.socketPath);\n\n // Registry scan scheduler only needs the registry DB — safe to start now.\n startRegistryScanScheduler();\n\n // Worker supervision reads the workers ledger off disk and pushes events to\n // owning sessions — no backend dependency, so it starts with the IPC server.\n startWorkerSupervisor();\n\n // Cache keepalive spawns trivial workers on the workers config — also no\n // backend dependency (observations store opportunistically when Postgres\n // is up), so it starts with the IPC server too.\n startCacheKeepalive();\n\n // Interactive-session cache keepalive: also no backend dependency (reads\n // AIBroker + transcript files, writes the workers ledger + its own state\n // file), so it starts with the IPC server too.\n startSessionKeepalive();\n\n // Connect the federation backend in the background. When storageBackend is\n // \"postgres\" this retries forever (never silently falls back to SQLite), so\n // the boot race with Docker/Postgres resolves itself once Postgres is up.\n // Only then do we start the federation-dependent schedulers and work queue —\n // this also keeps the first index run from firing against an unready backend.\n void createStorageBackend(config, { waitForPostgres: true })\n .then((backend) => {\n setStorageBackend(backend);\n process.stderr.write(\n `[pai-daemon] Federation backend: ${backend.backendType}\\n`\n );\n\n startIndexScheduler();\n\n void startLiveDashboard(config).then((h) => { dashboard = h; });\n\n if (backend.supportsPostgresFeatures) {\n startEmbedScheduler();\n\n // Repair fts_vector rows written under the old ('english') trigger.\n // Async, non-blocking: real tables hold millions of rows, and a\n // rebuild must never delay daemon startup. Idempotent — safe to\n // resume from scratch if interrupted (see fts-migration.ts).\n void migrateFtsConfig(backend).catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] FTS config migration failed: ${msg}\\n`);\n });\n } else {\n process.stderr.write(\n \"[pai-daemon] Embed scheduler: disabled (SQLite backend)\\n\"\n );\n }\n\n // Work queue — load persisted items and start worker loop\n loadQueue();\n startWorker();\n })\n .catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Fatal: Could not open federation storage: ${msg}\\n`\n );\n process.exit(1);\n });\n\n let dashboard: DashboardHandle | null = null;\n\n const shutdown = async (signal: string): Promise<void> => {\n process.stderr.write(`\\n[pai-daemon] ${signal} received. Stopping.\\n`);\n\n setShutdownRequested(true);\n\n if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);\n if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);\n if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);\n\n stopWorker();\n\n dashboard?.stop();\n server.close();\n\n const SHUTDOWN_TIMEOUT_MS = 10_000;\n const POLL_INTERVAL_MS = 100;\n const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\n `[pai-daemon] Waiting for in-progress operations to finish ` +\n `(index=${indexInProgress}, embed=${embedInProgress})...\\n`\n );\n\n while ((indexInProgress || embedInProgress) && Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Shutdown timeout reached — forcing exit.\\n\");\n } else {\n process.stderr.write(\"[pai-daemon] In-progress operations finished.\\n\");\n }\n }\n\n try {\n await storageBackend.close();\n } catch {\n // ignore\n }\n\n try {\n unlinkSync(config.socketPath);\n } catch {\n // ignore\n }\n\n removeDaemonPidfile();\n process.exit(0);\n };\n\n writeDaemonPidfile();\n process.on(\"SIGINT\", () => { shutdown(\"SIGINT\").catch(() => process.exit(0)); });\n process.on(\"SIGTERM\", () => { shutdown(\"SIGTERM\").catch(() => process.exit(0)); });\n\n // Keep process alive\n await new Promise(() => {});\n}\n"],"mappings":"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"}