@tekmidian/pai 0.60.1 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{async-CLvuY7b-.mjs → async-DNVWtQl2.mjs} +2 -2
- package/dist/{async-CLvuY7b-.mjs.map → async-DNVWtQl2.mjs.map} +1 -1
- package/dist/{chain-CGyAIHo4.mjs → chain-CuaDawK9.mjs} +4 -4
- package/dist/{chain-CGyAIHo4.mjs.map → chain-CuaDawK9.mjs.map} +1 -1
- package/dist/cli/index.mjs +13 -13
- package/dist/cli/program.mjs +13 -13
- package/dist/{context-handover-cache-BzM7Qudn.mjs → context-handover-cache-DFNnlWv2.mjs} +4 -4
- package/dist/{context-handover-cache-BzM7Qudn.mjs.map → context-handover-cache-DFNnlWv2.mjs.map} +1 -1
- package/dist/daemon/index.mjs +10 -10
- package/dist/{daemon-B_TodsIS.mjs → daemon-Bf7Eock4.mjs} +45 -24
- package/dist/{daemon-B_TodsIS.mjs.map → daemon-Bf7Eock4.mjs.map} +1 -1
- package/dist/daemon-bJi1gDkG.mjs +19 -0
- package/dist/daemon-mcp/index.mjs +8 -10
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{factory-EUUhSSoq.mjs → factory-BozZa81q.mjs} +3 -3
- package/dist/{factory-EUUhSSoq.mjs.map → factory-BozZa81q.mjs.map} +1 -1
- package/dist/{factory-DW7BCJJT.mjs → factory-UO9_DFMN.mjs} +1 -1
- package/dist/{fallback-CjdakQVu.mjs → fallback-F0TWGVM2.mjs} +11 -13
- package/dist/{fallback-CjdakQVu.mjs.map → fallback-F0TWGVM2.mjs.map} +1 -1
- package/dist/{helpers-D9WkIf9R.mjs → helpers-Dj-B314s.mjs} +3 -2
- package/dist/{helpers-D9WkIf9R.mjs.map → helpers-Dj-B314s.mjs.map} +1 -1
- package/dist/hooks/block-sleep-poll.mjs.map +2 -2
- package/dist/hooks/context-compression-hook.mjs.map +1 -1
- package/dist/hooks/load-project-context.mjs +8513 -133
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs.map +1 -1
- package/dist/hooks/route-agents-to-worker.mjs +3 -3
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +58 -58
- package/dist/hooks/security-validator.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +15 -15
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/update-tab-on-action.mjs +14 -12
- package/dist/hooks/update-tab-on-action.mjs.map +3 -3
- package/dist/hooks/update-tab-titles.mjs +13 -9
- package/dist/hooks/update-tab-titles.mjs.map +3 -3
- package/dist/hooks/worker-guard.mjs +7859 -0
- package/dist/hooks/worker-guard.mjs.map +7 -0
- package/dist/hooks/worker-proxy.mjs +3 -3
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +104 -11
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +3 -3
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{indexer-backend-DcDtin1O.mjs → indexer-backend-BN9rpp9O.mjs} +2 -2
- package/dist/{main-resolver-BFQfU5xo.mjs → main-resolver-BIft_FVF.mjs} +4 -4
- package/dist/{main-resolver-BFQfU5xo.mjs.map → main-resolver-BIft_FVF.mjs.map} +1 -1
- package/dist/{main-resolver-VMBENifT.mjs → main-resolver-C9-poMdi.mjs} +2 -2
- package/dist/{planner-DN967Lvm.mjs → planner-BG9ivPj0.mjs} +6 -6
- package/dist/{planner-DN967Lvm.mjs.map → planner-BG9ivPj0.mjs.map} +1 -1
- package/dist/{postgres-nvU5pShF.mjs → postgres-CxD6_Dca.mjs} +3 -3
- package/dist/{postgres-nvU5pShF.mjs.map → postgres-CxD6_Dca.mjs.map} +1 -1
- package/dist/{program-BnsjB57Q.mjs → program-BRvCzhjx.mjs} +249 -82
- package/dist/program-BRvCzhjx.mjs.map +1 -0
- package/dist/{run-FeM1_UVa.mjs → run-BVQZI79D.mjs} +610 -253
- package/dist/run-BVQZI79D.mjs.map +1 -0
- package/dist/{run-env-CEHTeIUM.mjs → run-env-BE5xyCwy.mjs} +25 -6
- package/dist/run-env-BE5xyCwy.mjs.map +1 -0
- package/dist/{server-CekGOJcp.mjs → server-BY6AedmC.mjs} +2 -2
- package/dist/{server-CekGOJcp.mjs.map → server-BY6AedmC.mjs.map} +1 -1
- package/dist/{session-keepalive-Bc9aT678.mjs → session-keepalive-DJA97K2X.mjs} +4 -4
- package/dist/{session-keepalive-Bc9aT678.mjs.map → session-keepalive-DJA97K2X.mjs.map} +1 -1
- package/dist/{sqlite-D9p5a4qe.mjs → sqlite-CfKfuggn.mjs} +2 -2
- package/dist/{sqlite-D9p5a4qe.mjs.map → sqlite-CfKfuggn.mjs.map} +1 -1
- package/dist/{tools-r-7ZG8B5.mjs → tools-B52IvaU6.mjs} +3 -3
- package/dist/{tools-qXyHFwNj.mjs → tools-DFdPbX-X.mjs} +2 -2
- package/dist/{tools-qXyHFwNj.mjs.map → tools-DFdPbX-X.mjs.map} +1 -1
- package/dist/{vault-indexer-DHYBpRZ5.mjs → vault-indexer-mCFus_AM.mjs} +2 -2
- package/dist/{vault-indexer-DHYBpRZ5.mjs.map → vault-indexer-mCFus_AM.mjs.map} +1 -1
- package/dist/{wakeup-C48hcN6O.mjs → wakeup-DO1O4udw.mjs} +2 -2
- package/dist/{wakeup-C48hcN6O.mjs.map → wakeup-DO1O4udw.mjs.map} +1 -1
- package/dist/{work-queue-worker-BL4dq5P7.mjs → work-queue-worker-BbrC3I_R.mjs} +2 -2
- package/dist/{work-queue-worker-BL4dq5P7.mjs.map → work-queue-worker-BbrC3I_R.mjs.map} +1 -1
- package/dist/work-queue-worker-z-AATZ2t.mjs +11 -0
- package/docs/commands/README.md +2 -0
- package/docs/commands/worker.md +28 -0
- package/package.json +2 -2
- package/scripts/build-hooks.mjs +6 -5
- package/scripts/build-skill-stubs.mjs +6 -5
- package/src/hooks/ts/lib/tab-title.ts +18 -0
- package/src/hooks/ts/lib/worker-guard.test.ts +188 -0
- package/src/hooks/ts/lib/worker-guard.ts +315 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +3 -20
- package/src/hooks/ts/pre-tool-use/worker-guard.ts +75 -0
- package/src/hooks/ts/session-start/load-project-context.ts +14 -0
- package/src/hooks/ts/stop/stop-hook.ts +4 -15
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -10
- package/dist/daemon-jviL3VVJ.mjs +0 -19
- package/dist/program-BnsjB57Q.mjs.map +0 -1
- package/dist/run-FeM1_UVa.mjs.map +0 -1
- package/dist/run-env-CEHTeIUM.mjs.map +0 -1
- package/dist/work-queue-worker-DVOVcEnO.mjs +0 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-keepalive-Bc9aT678.mjs","names":["fetchLiveSessionsDefault","sendToSessionDefault"],"sources":["../src/audit/session-usage.ts","../src/daemon/session-keepalive.ts"],"sourcesContent":["/**\n * session-usage.ts — parse the usage numbers out of a Claude Code session\n * (or subagent, or pai-worker event-mirror) JSONL transcript.\n *\n * All three log shapes share the same assistant-message envelope\n * ({ type: \"assistant\", message: { id, model, usage } }), so one parser\n * covers `pai audit tokens session` and the per-log readings feeding\n * `pai audit tokens spawn`.\n *\n * Streaming writes one JSONL line per content block of the same logical\n * turn, repeating message.id and usage each time — summing every line would\n * multiply usage by the block count. Keeping only the first line seen per\n * message.id is what the reference script (and this parser) count instead.\n */\n\nimport { createReadStream, existsSync } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\nexport interface UsageTotals {\n cache_read_input_tokens: number;\n cache_creation_input_tokens: number;\n input_tokens: number;\n output_tokens: number;\n}\n\nexport interface CacheCreationSplit {\n ephemeral5m: number;\n ephemeral1h: number;\n}\n\nexport interface CompactionEvent {\n trigger: string;\n preTokens: number;\n turnIndex: number;\n}\n\nexport interface ModelSwitch {\n turnIndex: number;\n from: string;\n to: string;\n cacheRead: number;\n cacheCreation: number;\n}\n\nexport interface FallbackEvent {\n turnIndex: number;\n from: string;\n to: string;\n category: string;\n scope: string;\n}\n\nexport interface SessionUsageReport {\n path: string;\n sizeBytes: number;\n turns: number;\n totals: UsageTotals;\n /** Per-model assistant-turn counts. */\n models: Record<string, number>;\n /** cache_read + cache_creation + input on the first / last assistant turn seen. */\n firstTurnContext: number | null;\n lastTurnContext: number | null;\n cacheCreationSplit: CacheCreationSplit;\n /** Average / max of the per-turn context value across all turns; null if no turns. */\n avgContext: number | null;\n maxContext: number | null;\n /** Turns whose per-turn context value exceeds the report's threshold. */\n turnsAboveThreshold: number;\n /** Turns whose cache_creation_input_tokens exceeds 20000. */\n cacheRebuildTurns: number;\n /** Real human-authored user prompts (excludes tool-result-only \"user\" lines). */\n userPrompts: number;\n /**\n * Sum over assistant turns of the real user prompts seen before that turn;\n * multiplied by per-prompt hook tokens it gives the tokens UserPromptSubmit\n * output occupied across the whole session.\n */\n promptExposure: number;\n compactions: CompactionEvent[];\n /** ISO timestamp of the first assistant turn. */\n firstTurnAt: string | null;\n /** Model of the last folded turn; drives switch detection. */\n lastModel: string | null;\n modelSwitches: ModelSwitch[];\n fallbacks: FallbackEvent[];\n /** Timestamp (ms) of the last folded assistant turn; drives idle-gap detection. */\n lastTurnAtMs: number | null;\n /** Gaps between consecutive assistant turns exceeding 60 minutes — evidence\n * for whether the session ever went idle long enough to risk its cache TTL\n * (see sessions.cacheKeepalive, src/daemon/config.ts). */\n idleGapsOver60min: number;\n /** Real user prompts whose text is exactly the configured keepalive word\n * (set only when parseSessionUsage is called with one); null otherwise. */\n keepaliveBeats: number | null;\n}\n\nconst USAGE_KEYS: (keyof UsageTotals)[] = [\n \"cache_read_input_tokens\",\n \"cache_creation_input_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n];\n\nfunction emptyTotals(): UsageTotals {\n return { cache_read_input_tokens: 0, cache_creation_input_tokens: 0, input_tokens: 0, output_tokens: 0 };\n}\n\nexport interface AssistantLine {\n type?: string;\n uuid?: string;\n subtype?: string;\n /** Claude Code marks skill expansions and injected system reminders isMeta; they fire no UserPromptSubmit hook. */\n isMeta?: boolean;\n /** Present on newer logs: { kind: \"human\" } for a typed prompt. */\n origin?: { kind?: string };\n compactMetadata?: { trigger?: string; preTokens?: number };\n timestamp?: string;\n originalModel?: string;\n fallbackModel?: string;\n apiRefusalCategory?: string;\n scope?: string;\n message?: {\n id?: string;\n model?: string;\n usage?: Record<string, unknown> & {\n cache_creation?: { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number };\n };\n content?: string | Array<{ type?: string }>;\n stop_reason?: string | null;\n };\n}\n\n/**\n * `type:\"system\"` `subtype:\"compact_boundary\"` lines mark a compaction.\n * turnIndex is the count of assistant turns already folded when it fired,\n * so it lines up with the turn numbering the text/JSON report prints.\n */\nexport function isCompactBoundary(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"compact_boundary\";\n}\n\nexport function parseCompactionEvent(line: AssistantLine, turnIndex: number): CompactionEvent | null {\n const meta = line.compactMetadata;\n if (!meta || typeof meta.preTokens !== \"number\") return null;\n return { trigger: meta.trigger ?? \"unknown\", preTokens: meta.preTokens, turnIndex };\n}\n\n/**\n * `type:\"system\"` `subtype:\"model_refusal_fallback\"` lines mark an\n * automatic model switch fired by an API safety refusal (e.g. \"cyber\"\n * category), not a user or config choice — worth flagging separately since\n * it can rebuild the whole prompt cache mid-session.\n */\nexport function isModelFallback(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"model_refusal_fallback\";\n}\n\nexport function parseFallbackEvent(line: AssistantLine, turnIndex: number): FallbackEvent {\n return {\n turnIndex,\n from: line.originalModel ?? \"unknown\",\n to: line.fallbackModel ?? \"unknown\",\n category: line.apiRefusalCategory ?? \"unknown\",\n scope: line.scope ?? \"unknown\",\n };\n}\n\n/**\n * Fold one already-parsed JSONL line into a report being accumulated.\n * Exported separately so both the streaming file reader below and tests\n * (which build fixtures as arrays of objects, not files) share one path.\n * Returns the turn's context value when a new turn was counted, else null\n * (non-assistant line, no usage, or a duplicate message.id already seen).\n */\nexport function foldAssistantLine(\n report: Pick<\n SessionUsageReport,\n | \"turns\"\n | \"totals\"\n | \"models\"\n | \"firstTurnContext\"\n | \"lastTurnContext\"\n | \"cacheCreationSplit\"\n | \"maxContext\"\n | \"turnsAboveThreshold\"\n | \"cacheRebuildTurns\"\n | \"firstTurnAt\"\n | \"lastModel\"\n | \"modelSwitches\"\n | \"lastTurnAtMs\"\n | \"idleGapsOver60min\"\n >,\n seenIds: Set<string>,\n line: AssistantLine,\n threshold: number\n): number | null {\n if (line.type !== \"assistant\") return null;\n const message = line.message;\n const usage = message?.usage;\n if (!usage) return null;\n const id = message?.id ?? line.uuid;\n if (id) {\n if (seenIds.has(id)) return null;\n seenIds.add(id);\n }\n report.turns++;\n for (const key of USAGE_KEYS) {\n const v = usage[key];\n if (typeof v === \"number\") report.totals[key] += v;\n }\n const context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n if (report.firstTurnContext === null) {\n report.firstTurnContext = context;\n report.firstTurnAt = line.timestamp ?? null;\n }\n const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;\n if (!Number.isNaN(atMs)) {\n if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 60 * 60 * 1000) {\n report.idleGapsOver60min++;\n }\n report.lastTurnAtMs = atMs;\n }\n report.lastTurnContext = context;\n report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);\n if (context > threshold) report.turnsAboveThreshold++;\n if ((Number(usage.cache_creation_input_tokens) || 0) > 20000) report.cacheRebuildTurns++;\n const model = message?.model ?? \"unknown\";\n report.models[model] = (report.models[model] ?? 0) + 1;\n if (report.lastModel !== null && report.lastModel !== model) {\n report.modelSwitches.push({\n turnIndex: report.turns,\n from: report.lastModel,\n to: model,\n cacheRead: Number(usage.cache_read_input_tokens) || 0,\n cacheCreation: Number(usage.cache_creation_input_tokens) || 0,\n });\n }\n report.lastModel = model;\n const split = usage.cache_creation;\n if (split) {\n report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;\n report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;\n }\n return context;\n}\n\n/**\n * True for a real human-authored `type:\"user\"` prompt line: content is a\n * plain string, or a content array with no `tool_result` block. Claude Code\n * encodes tool results as `type:\"user\"` messages whose content array is\n * entirely (or partly) tool_result blocks — those must not count as prompts.\n */\nexport function isRealUserPrompt(line: AssistantLine): boolean {\n if (line.type !== \"user\") return false;\n if (line.isMeta) return false;\n if (line.origin?.kind && line.origin.kind !== \"human\") return false;\n const content = line.message?.content;\n if (typeof content === \"string\") return true;\n if (Array.isArray(content)) return !content.some((block) => block?.type === \"tool_result\");\n return false;\n}\n\n/** Plain text of a real user prompt: the string content, or its first text block. */\nexport function extractUserPromptText(line: AssistantLine): string {\n const content = line.message?.content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const block = content.find((b) => b?.type === \"text\") as { text?: string } | undefined;\n return block?.text ?? \"\";\n }\n return \"\";\n}\n\n/**\n * Parse a session/subagent/worker-event JSONL file into a usage report.\n * `keepaliveWord`, when given, counts real user prompts whose text exactly\n * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into\n * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.\n */\nexport async function parseSessionUsage(\n path: string,\n threshold = 200_000,\n keepaliveWord?: string\n): Promise<SessionUsageReport> {\n const report: SessionUsageReport = {\n path,\n sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,\n turns: 0,\n totals: emptyTotals(),\n models: {},\n firstTurnContext: null,\n lastTurnContext: null,\n cacheCreationSplit: { ephemeral5m: 0, ephemeral1h: 0 },\n avgContext: null,\n maxContext: null,\n turnsAboveThreshold: 0,\n cacheRebuildTurns: 0,\n userPrompts: 0,\n promptExposure: 0,\n compactions: [],\n firstTurnAt: null,\n lastModel: null,\n modelSwitches: [],\n fallbacks: [],\n lastTurnAtMs: null,\n idleGapsOver60min: 0,\n keepaliveBeats: keepaliveWord ? 0 : null,\n };\n const seenIds = new Set<string>();\n let contextSum = 0;\n\n const rl = createInterface({ input: createReadStream(path, \"utf8\"), crlfDelay: Infinity });\n for await (const raw of rl) {\n if (!raw.trim()) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (obj.type === \"user\") {\n if (isRealUserPrompt(obj)) {\n report.userPrompts++;\n if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) {\n report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;\n }\n }\n continue;\n }\n if (isCompactBoundary(obj)) {\n const event = parseCompactionEvent(obj, report.turns);\n if (event) report.compactions.push(event);\n continue;\n }\n if (isModelFallback(obj)) {\n report.fallbacks.push(parseFallbackEvent(obj, report.turns));\n continue;\n }\n const context = foldAssistantLine(report, seenIds, obj, threshold);\n if (context !== null) {\n contextSum += context;\n report.promptExposure += report.userPrompts;\n }\n }\n report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;\n return report;\n}\n\nexport function totalUsageTokens(totals: UsageTotals): number {\n return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;\n}\n","/**\n * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live\n * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts\n * and docs/cache-keepalive.md, \"Interactive sessions\").\n *\n * One beat = one trivial prompt typed into a session through AIBroker's\n * send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`\n * so the target sees only the bare word typed into its input line and\n * nothing is queued into its mailbox as a peer message demanding a reply.\n * A cache READ refreshes the provider's ephemeral prompt-cache TTL at\n * roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next\n * real prompt.\n *\n * Distinct from workers/keepalive.ts, which beats a *worker provider's*\n * cache on a fixed timer regardless of activity: this only beats a session\n * that has actually been idle long enough to be at risk, and only inside a\n * configured working-hours window, so it never fires while the user is\n * present at the keyboard or overnight when nobody will read the reply\n * before the cache would have expired anyway.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n fetchLiveSessions as fetchLiveSessionsDefault,\n sendToSession as sendToSessionDefault,\n type AiBrokerSessionMeta,\n} from \"../cli/lib/aibroker-client.js\";\nimport { writeJsonAtomic } from \"../config/json-store.js\";\nimport { paiHomePath } from \"../config/pai-home.js\";\nimport { appendLedger } from \"../workers/ledger.js\";\nimport { readWorkersSection } from \"../workers/config.js\";\nimport { workersLogDir } from \"../workers/paths.js\";\nimport { worktreesDir } from \"../workers/worktree.js\";\nimport { itermUuid, sessionMapPath, type SessionMapEntry } from \"../workers/scope.js\";\nimport { isRealUserPrompt, extractUserPromptText, type AssistantLine } from \"../audit/session-usage.js\";\nimport type { SessionsCacheKeepaliveConfig } from \"./config.js\";\n\n// ---------------------------------------------------------------------------\n// State file — per-session beat counters, rebuildable (see json-store.ts on\n// when NOT to use readJsonStrict: a damaged file here just resets counters,\n// never blocks the feature).\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveEntry {\n /** Beats sent since the last real (non-keepalive) user prompt. */\n beats: number;\n /** Identity of the last real user prompt observed, so a fresh one can be\n * told apart from the keepalive's own echo landing back in the transcript. */\n lastRealPromptKey: string | null;\n /** ISO stamp of the last beat sent. */\n lastBeatAt: string | null;\n}\n\nexport type SessionKeepaliveState = Record<string, SessionKeepaliveEntry>;\n\nexport function sessionKeepaliveStatePath(): string {\n return paiHomePath(\"session-keepalive.json\");\n}\n\nfunction emptyEntry(): SessionKeepaliveEntry {\n return { beats: 0, lastRealPromptKey: null, lastBeatAt: null };\n}\n\nexport function loadSessionKeepaliveState(path: string = sessionKeepaliveStatePath()): SessionKeepaliveState {\n if (!existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as SessionKeepaliveState;\n } catch {\n return {};\n }\n}\n\nexport function saveSessionKeepaliveState(\n state: SessionKeepaliveState,\n path: string = sessionKeepaliveStatePath()\n): void {\n writeJsonAtomic(path, state, { backup: false, label: path });\n}\n\n// ---------------------------------------------------------------------------\n// Active-hours window\n// ---------------------------------------------------------------------------\n\n/** Parse \"HH:MM-HH:MM\" into minutes-since-midnight. Throws on a malformed\n * spec — an explicit config error beats a window that is silently always\n * on or always off. */\nexport function parseActiveHours(spec: string): { startMin: number; endMin: number } {\n const m = spec.match(/^(\\d{1,2}):(\\d{2})-(\\d{1,2}):(\\d{2})$/);\n if (!m) {\n throw new Error(`sessions.cacheKeepalive.activeHours: invalid \"${spec}\" (want \"HH:MM-HH:MM\")`);\n }\n return {\n startMin: Number(m[1]) * 60 + Number(m[2]),\n endMin: Number(m[3]) * 60 + Number(m[4]),\n };\n}\n\n/**\n * Whether `now` (local time) sits inside the window. A window that wraps\n * midnight (e.g. \"22:00-06:00\") is honoured by inverting the test instead of\n * requiring startMin < endMin.\n */\nexport function isWithinActiveHours(now: Date, spec: string): boolean {\n const { startMin, endMin } = parseActiveHours(spec);\n const nowMin = now.getHours() * 60 + now.getMinutes();\n if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;\n return nowMin >= startMin || nowMin < endMin;\n}\n\n// ---------------------------------------------------------------------------\n// Transcript lookup\n// ---------------------------------------------------------------------------\n\n/**\n * Full path to a live session's transcript under ~/.claude/projects, or null\n * when none is found — the case for a session too new to have written a file\n * yet. A worker running in a worktree DOES write a transcript here (Claude\n * Code encodes its worktree cwd as the project dir name), so workers are not\n * excluded \"for free\" — see isWorkerSession.\n */\nexport function findSessionTranscript(\n sessionId: string,\n projectsDir: string = join(homedir(), \".claude\", \"projects\")\n): string | null {\n let projectDirs: string[];\n try {\n projectDirs = readdirSync(projectsDir);\n } catch {\n return null;\n }\n for (const projectDir of projectDirs) {\n const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id\n * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`\n * transcripts are named after — the two are unrelated identifiers. The\n * status line bridges them on every refresh via `claude-session-map.json`\n * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the\n * map entries whose `term` pane UUID matches, the most recently written one.\n * Returns null when the pane has no (fresh enough) mapped Claude session.\n */\nexport function resolveClaudeSessionIdFromMap(paneId: string, logDir: string): string | null {\n const path = sessionMapPath(logDir);\n if (!paneId || !existsSync(path)) return null;\n let map: Record<string, SessionMapEntry>;\n try {\n map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n } catch {\n return null;\n }\n let best: SessionMapEntry | null = null;\n for (const entry of Object.values(map)) {\n if (!entry.term || itermUuid(entry.term) !== paneId) continue;\n if (!best || entry.ts > best.ts) best = entry;\n }\n return best?.session ?? null;\n}\n\n/** Claude Code's project-dir encoding of a cwd: every \"/\" becomes \"-\". */\nfunction encodeProjectDirName(cwd: string): string {\n return cwd.replace(/\\//g, \"-\");\n}\n\n/**\n * Is this live \"claude\"-kind session actually a worker pane rather than an\n * interactive one? `claude -p` workers running in a worktree write their\n * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),\n * so a missing transcript is NOT how workers get excluded — this predicate\n * is. Either signal is enough:\n * (a) the transcript's project-dir name is the encoded form of a path\n * under <logDir>/worktrees (every worker worktree lives there), or\n * (b) the broker's session name/paiName names a path under the workers\n * log dir or one of its worktrees (best-effort: AIBroker does not\n * guarantee this, but honors it when present).\n */\nexport function isWorkerSession(\n meta: AiBrokerSessionMeta,\n transcriptPath: string | null,\n logDir: string\n): boolean {\n const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));\n if (transcriptPath) {\n const projectDirName = transcriptPath.split(\"/\").slice(0, -1).pop() ?? \"\";\n if (projectDirName.startsWith(worktreesPrefix)) return true;\n }\n const rawPrefix = worktreesDir(logDir);\n for (const field of [meta.name, meta.paiName]) {\n if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Tail snapshot: last turn's context, mid-turn flag, last real prompt\n// ---------------------------------------------------------------------------\n\nexport interface TranscriptSnapshot {\n /** cache_read + cache_creation + input on the most recent assistant turn seen. */\n context: number | null;\n /** True when the last line in the file shows the session still working: an\n * assistant turn with no usage yet (streaming) or a stop_reason other than\n * end_turn/stop_sequence (e.g. mid tool-call), or a real user prompt with\n * no reply behind it yet. A beat sent now would land mid-generation. */\n midTurn: boolean;\n /** The last real (human-authored) user prompt seen, if any. */\n lastRealPrompt: { key: string; text: string } | null;\n}\n\nfunction isMidTurn(line: AssistantLine | null): boolean {\n if (!line) return false;\n if (line.type === \"user\" && isRealUserPrompt(line)) return true;\n if (line.type === \"assistant\") {\n const stopReason = line.message?.stop_reason;\n if (stopReason === undefined || stopReason === null) return true;\n return stopReason !== \"end_turn\" && stopReason !== \"stop_sequence\";\n }\n return false;\n}\n\nexport function readTranscriptSnapshot(path: string): TranscriptSnapshot {\n let lines: string[];\n try {\n lines = readFileSync(path, \"utf8\").split(\"\\n\");\n } catch {\n return { context: null, midTurn: false, lastRealPrompt: null };\n }\n\n let lastLine: AssistantLine | null = null;\n let context: number | null = null;\n let lastRealPrompt: { key: string; text: string } | null = null;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const raw = lines[i].trim();\n if (!raw) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (lastLine === null) lastLine = obj;\n\n if (context === null && obj.type === \"assistant\" && obj.message?.usage) {\n const usage = obj.message.usage;\n context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n }\n\n if (lastRealPrompt === null && isRealUserPrompt(obj)) {\n const key = obj.uuid ?? obj.timestamp ?? String(i);\n lastRealPrompt = { key, text: extractUserPromptText(obj).trim() };\n }\n\n if (context !== null && lastRealPrompt !== null) break;\n }\n\n return { context, midTurn: isMidTurn(lastLine), lastRealPrompt };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger — same file WORKER-KEEPALIVE lines go to (workers.logDir/ledger.log)\n// ---------------------------------------------------------------------------\n\n/** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */\nexport const SESSION_KEEPALIVE_EVENT = \"SESSION-KEEPALIVE\";\n\nexport function sessionKeepaliveLedgerPath(): string {\n const { workers } = readWorkersSection();\n return join(workersLogDir(workers), \"ledger.log\");\n}\n\n/** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */\nexport function sessionKeepaliveLedgerSummary(\n path: string = sessionKeepaliveLedgerPath(),\n lastN = 10\n): { sent: number; skipped: number; lastLines: string[] } {\n if (!existsSync(path)) return { sent: 0, skipped: 0, lastLines: [] };\n const lines = readFileSync(path, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));\n const sent = lines.filter((l) => / result=sent(\\s|$)/.test(l)).length;\n return { sent, skipped: lines.length - sent, lastLines: lines.slice(-lastN) };\n}\n\n// ---------------------------------------------------------------------------\n// The tick\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveDeps {\n now: () => Date;\n fetchLiveSessions: () => Promise<AiBrokerSessionMeta[]>;\n /** AIBroker pane id (fetchLiveSessions' `sessionId`) -> Claude transcript\n * session id, via the status line's claude-session-map.json bridge. */\n resolveClaudeSessionId: (paneId: string) => string | null;\n findTranscript: (sessionId: string) => string | null;\n mtimeMs: (path: string) => number | null;\n readSnapshot: (path: string) => TranscriptSnapshot;\n sendBeat: (sessionId: string, text: string) => Promise<{ ok: boolean; error?: string }>;\n loadState: () => SessionKeepaliveState;\n saveState: (s: SessionKeepaliveState) => void;\n ledger: (kv: Record<string, string | number | null | undefined>) => void;\n}\n\nexport interface SessionKeepaliveResult {\n sessionId: string;\n result: \"sent\" | string; // \"skipped:<reason>\"\n}\n\nfunction defaultDeps(): SessionKeepaliveDeps {\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n return {\n now: () => new Date(),\n fetchLiveSessions: fetchLiveSessionsDefault,\n resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),\n findTranscript: (id) => findSessionTranscript(id),\n mtimeMs: (p) => {\n try {\n return statSync(p).mtimeMs;\n } catch {\n return null;\n }\n },\n readSnapshot: readTranscriptSnapshot,\n sendBeat: (id, text) => sendToSessionDefault(id, text, undefined, { noReply: true }),\n loadState: () => loadSessionKeepaliveState(),\n saveState: (s) => saveSessionKeepaliveState(s),\n ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv),\n };\n}\n\n/**\n * One tick over every live interactive session: beat the ones that qualify,\n * skip (with a reason) the ones that don't. Returns one result per live\n * \"claude\"-kind session for tests to assert against; writes the ledger line\n * and (when any counter changed) the state file as side effects.\n */\nexport async function runSessionKeepaliveTick(\n config: SessionsCacheKeepaliveConfig,\n overrides: Partial<SessionKeepaliveDeps> = {}\n): Promise<SessionKeepaliveResult[]> {\n if (!config.enabled) return [];\n\n const d: SessionKeepaliveDeps = { ...defaultDeps(), ...overrides };\n const sessions = await d.fetchLiveSessions();\n const state = d.loadState();\n const now = d.now();\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n const results: SessionKeepaliveResult[] = [];\n let anyChanged = false;\n\n for (const s of sessions) {\n if (s.kind !== \"claude\") continue;\n const paneId = s.sessionId;\n\n const claudeId = d.resolveClaudeSessionId(paneId);\n if (!claudeId) {\n d.ledger({ session: null, pane: paneId, result: \"skipped:unmapped\" });\n results.push({ sessionId: paneId, result: \"skipped:unmapped\" });\n continue;\n }\n const sessionId = claudeId;\n const entry = { ...(state[sessionId] ?? emptyEntry()) };\n let changed = false;\n\n const transcript = d.findTranscript(sessionId);\n\n if (isWorkerSession(s, transcript, logDir)) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:worker\" });\n results.push({ sessionId, result: \"skipped:worker\" });\n continue;\n }\n\n if (!transcript) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:no-transcript\" });\n results.push({ sessionId, result: \"skipped:no-transcript\" });\n continue;\n }\n\n const snapshot = d.readSnapshot(transcript);\n\n // A fresh real prompt (not the keepalive's own echo) resets the beat\n // count — the idle stretch it capped is over.\n if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {\n entry.lastRealPromptKey = snapshot.lastRealPrompt.key;\n if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;\n changed = true;\n }\n\n const mtime = d.mtimeMs(transcript);\n const idleMin = mtime === null ? null : (now.getTime() - mtime) / 60_000;\n\n const skip: string | null =\n mtime === null || idleMin === null\n ? \"no-mtime\"\n : idleMin < config.idleMinutes\n ? `idle:${idleMin.toFixed(1)}min`\n : !isWithinActiveHours(now, config.activeHours)\n ? \"hours\"\n : snapshot.context === null || snapshot.context < config.minContextTokens\n ? \"context\"\n : snapshot.midTurn\n ? \"mid-turn\"\n : entry.beats >= config.maxBeats\n ? \"max-beats\"\n : null;\n\n const ledgerBase = {\n session: sessionId,\n pane: paneId,\n idle_min: idleMin === null ? null : idleMin.toFixed(1),\n context: snapshot.context,\n };\n\n if (skip) {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: `skipped:${skip}` });\n results.push({ sessionId, result: `skipped:${skip}` });\n } else {\n const sent = await d.sendBeat(paneId, config.prompt);\n if (sent.ok) {\n entry.beats += 1;\n entry.lastBeatAt = now.toISOString();\n changed = true;\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"sent\" });\n results.push({ sessionId, result: \"sent\" });\n } else {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"skipped:send-failed\" });\n results.push({ sessionId, result: \"skipped:send-failed\" });\n }\n }\n\n if (changed) {\n state[sessionId] = entry;\n anyChanged = true;\n }\n }\n\n if (anyChanged) d.saveState(state);\n return results;\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,MAAM,aAAoC;CACxC;CACA;CACA;CACA;CACD;AAED,SAAS,cAA2B;AAClC,QAAO;EAAE,yBAAyB;EAAG,6BAA6B;EAAG,cAAc;EAAG,eAAe;EAAG;;;;;;;AAiC1G,SAAgB,kBAAkB,MAA8B;AAC9D,QAAO,KAAK,SAAS,YAAY,KAAK,YAAY;;AAGpD,SAAgB,qBAAqB,MAAqB,WAA2C;CACnG,MAAM,OAAO,KAAK;AAClB,KAAI,CAAC,QAAQ,OAAO,KAAK,cAAc,SAAU,QAAO;AACxD,QAAO;EAAE,SAAS,KAAK,WAAW;EAAW,WAAW,KAAK;EAAW;EAAW;;;;;;;;AASrF,SAAgB,gBAAgB,MAA8B;AAC5D,QAAO,KAAK,SAAS,YAAY,KAAK,YAAY;;AAGpD,SAAgB,mBAAmB,MAAqB,WAAkC;AACxF,QAAO;EACL;EACA,MAAM,KAAK,iBAAiB;EAC5B,IAAI,KAAK,iBAAiB;EAC1B,UAAU,KAAK,sBAAsB;EACrC,OAAO,KAAK,SAAS;EACtB;;;;;;;;;AAUH,SAAgB,kBACd,QAiBA,SACA,MACA,WACe;AACf,KAAI,KAAK,SAAS,YAAa,QAAO;CACtC,MAAM,UAAU,KAAK;CACrB,MAAM,QAAQ,SAAS;AACvB,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,KAAK,SAAS,MAAM,KAAK;AAC/B,KAAI,IAAI;AACN,MAAI,QAAQ,IAAI,GAAG,CAAE,QAAO;AAC5B,UAAQ,IAAI,GAAG;;AAEjB,QAAO;AACP,MAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,IAAI,MAAM;AAChB,MAAI,OAAO,MAAM,SAAU,QAAO,OAAO,QAAQ;;CAEnD,MAAM,WACH,OAAO,MAAM,wBAAwB,IAAI,MACzC,OAAO,MAAM,4BAA4B,IAAI,MAC7C,OAAO,MAAM,aAAa,IAAI;AACjC,KAAI,OAAO,qBAAqB,MAAM;AACpC,SAAO,mBAAmB;AAC1B,SAAO,cAAc,KAAK,aAAa;;CAEzC,MAAM,OAAO,KAAK,YAAY,KAAK,MAAM,KAAK,UAAU,GAAG;AAC3D,KAAI,CAAC,OAAO,MAAM,KAAK,EAAE;AACvB,MAAI,OAAO,iBAAiB,QAAQ,OAAO,OAAO,eAAe,OAAU,IACzE,QAAO;AAET,SAAO,eAAe;;AAExB,QAAO,kBAAkB;AACzB,QAAO,aAAa,OAAO,eAAe,OAAO,UAAU,KAAK,IAAI,OAAO,YAAY,QAAQ;AAC/F,KAAI,UAAU,UAAW,QAAO;AAChC,MAAK,OAAO,MAAM,4BAA4B,IAAI,KAAK,IAAO,QAAO;CACrE,MAAM,QAAQ,SAAS,SAAS;AAChC,QAAO,OAAO,UAAU,OAAO,OAAO,UAAU,KAAK;AACrD,KAAI,OAAO,cAAc,QAAQ,OAAO,cAAc,MACpD,QAAO,cAAc,KAAK;EACxB,WAAW,OAAO;EAClB,MAAM,OAAO;EACb,IAAI;EACJ,WAAW,OAAO,MAAM,wBAAwB,IAAI;EACpD,eAAe,OAAO,MAAM,4BAA4B,IAAI;EAC7D,CAAC;AAEJ,QAAO,YAAY;CACnB,MAAM,QAAQ,MAAM;AACpB,KAAI,OAAO;AACT,SAAO,mBAAmB,eAAe,OAAO,MAAM,0BAA0B,IAAI;AACpF,SAAO,mBAAmB,eAAe,OAAO,MAAM,0BAA0B,IAAI;;AAEtF,QAAO;;;;;;;;AAST,SAAgB,iBAAiB,MAA8B;AAC7D,KAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,KAAI,KAAK,OAAQ,QAAO;AACxB,KAAI,KAAK,QAAQ,QAAQ,KAAK,OAAO,SAAS,QAAS,QAAO;CAC9D,MAAM,UAAU,KAAK,SAAS;AAC9B,KAAI,OAAO,YAAY,SAAU,QAAO;AACxC,KAAI,MAAM,QAAQ,QAAQ,CAAE,QAAO,CAAC,QAAQ,MAAM,UAAU,OAAO,SAAS,cAAc;AAC1F,QAAO;;;AAIT,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,UAAU,KAAK,SAAS;AAC9B,KAAI,OAAO,YAAY,SAAU,QAAO;AACxC,KAAI,MAAM,QAAQ,QAAQ,CAExB,QADc,QAAQ,MAAM,MAAM,GAAG,SAAS,OAAO,EACvC,QAAQ;AAExB,QAAO;;;;;;;;AAST,eAAsB,kBACpB,MACA,YAAY,KACZ,eAC6B;CAC7B,MAAM,SAA6B;EACjC;EACA,WAAW,WAAW,KAAK,GAAG,aAAa,KAAK,CAAC,aAAa;EAC9D,OAAO;EACP,QAAQ,aAAa;EACrB,QAAQ,EAAE;EACV,kBAAkB;EAClB,iBAAiB;EACjB,oBAAoB;GAAE,aAAa;GAAG,aAAa;GAAG;EACtD,YAAY;EACZ,YAAY;EACZ,qBAAqB;EACrB,mBAAmB;EACnB,aAAa;EACb,gBAAgB;EAChB,aAAa,EAAE;EACf,aAAa;EACb,WAAW;EACX,eAAe,EAAE;EACjB,WAAW,EAAE;EACb,cAAc;EACd,mBAAmB;EACnB,gBAAgB,gBAAgB,IAAI;EACrC;CACD,MAAM,0BAAU,IAAI,KAAa;CACjC,IAAI,aAAa;CAEjB,MAAM,KAAK,gBAAgB;EAAE,OAAO,iBAAiB,MAAM,OAAO;EAAE,WAAW;EAAU,CAAC;AAC1F,YAAW,MAAM,OAAO,IAAI;AAC1B,MAAI,CAAC,IAAI,MAAM,CAAE;EACjB,IAAI;AACJ,MAAI;AACF,SAAM,KAAK,MAAM,IAAI;UACf;AACN;;AAEF,MAAI,IAAI,SAAS,QAAQ;AACvB,OAAI,iBAAiB,IAAI,EAAE;AACzB,WAAO;AACP,QAAI,iBAAiB,sBAAsB,IAAI,CAAC,MAAM,KAAK,cACzD,QAAO,kBAAkB,OAAO,kBAAkB,KAAK;;AAG3D;;AAEF,MAAI,kBAAkB,IAAI,EAAE;GAC1B,MAAM,QAAQ,qBAAqB,KAAK,OAAO,MAAM;AACrD,OAAI,MAAO,QAAO,YAAY,KAAK,MAAM;AACzC;;AAEF,MAAI,gBAAgB,IAAI,EAAE;AACxB,UAAO,UAAU,KAAK,mBAAmB,KAAK,OAAO,MAAM,CAAC;AAC5D;;EAEF,MAAM,UAAU,kBAAkB,QAAQ,SAAS,KAAK,UAAU;AAClE,MAAI,YAAY,MAAM;AACpB,iBAAc;AACd,UAAO,kBAAkB,OAAO;;;AAGpC,QAAO,aAAa,OAAO,QAAQ,IAAI,KAAK,MAAM,aAAa,OAAO,MAAM,GAAG;AAC/E,QAAO;;AAGT,SAAgB,iBAAiB,QAA6B;AAC5D,QAAO,OAAO,0BAA0B,OAAO,8BAA8B,OAAO,eAAe,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;ACxS5G,SAAgB,4BAAoC;AAClD,QAAO,YAAY,yBAAyB;;AAG9C,SAAS,aAAoC;AAC3C,QAAO;EAAE,OAAO;EAAG,mBAAmB;EAAM,YAAY;EAAM;;AAGhE,SAAgB,0BAA0B,OAAe,2BAA2B,EAAyB;AAC3G,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO,EAAE;AAChC,KAAI;AACF,SAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;SACvC;AACN,SAAO,EAAE;;;AAIb,SAAgB,0BACd,OACA,OAAe,2BAA2B,EACpC;AACN,iBAAgB,MAAM,OAAO;EAAE,QAAQ;EAAO,OAAO;EAAM,CAAC;;;;;AAU9D,SAAgB,iBAAiB,MAAoD;CACnF,MAAM,IAAI,KAAK,MAAM,wCAAwC;AAC7D,KAAI,CAAC,EACH,OAAM,IAAI,MAAM,iDAAiD,KAAK,wBAAwB;AAEhG,QAAO;EACL,UAAU,OAAO,EAAE,GAAG,GAAG,KAAK,OAAO,EAAE,GAAG;EAC1C,QAAQ,OAAO,EAAE,GAAG,GAAG,KAAK,OAAO,EAAE,GAAG;EACzC;;;;;;;AAQH,SAAgB,oBAAoB,KAAW,MAAuB;CACpE,MAAM,EAAE,UAAU,WAAW,iBAAiB,KAAK;CACnD,MAAM,SAAS,IAAI,UAAU,GAAG,KAAK,IAAI,YAAY;AACrD,KAAI,YAAY,OAAQ,QAAO,UAAU,YAAY,SAAS;AAC9D,QAAO,UAAU,YAAY,SAAS;;;;;;;;;AAcxC,SAAgB,sBACd,WACA,cAAsB,KAAK,SAAS,EAAE,WAAW,WAAW,EAC7C;CACf,IAAI;AACJ,KAAI;AACF,gBAAc,YAAY,YAAY;SAChC;AACN,SAAO;;AAET,MAAK,MAAM,cAAc,aAAa;EACpC,MAAM,YAAY,KAAK,aAAa,YAAY,GAAG,UAAU,QAAQ;AACrE,MAAI,WAAW,UAAU,CAAE,QAAO;;AAEpC,QAAO;;;;;;;;;;;AAYT,SAAgB,8BAA8B,QAAgB,QAA+B;CAC3F,MAAM,OAAO,eAAe,OAAO;AACnC,KAAI,CAAC,UAAU,CAAC,WAAW,KAAK,CAAE,QAAO;CACzC,IAAI;AACJ,KAAI;AACF,QAAM,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;SACtC;AACN,SAAO;;CAET,IAAI,OAA+B;AACnC,MAAK,MAAM,SAAS,OAAO,OAAO,IAAI,EAAE;AACtC,MAAI,CAAC,MAAM,QAAQ,UAAU,MAAM,KAAK,KAAK,OAAQ;AACrD,MAAI,CAAC,QAAQ,MAAM,KAAK,KAAK,GAAI,QAAO;;AAE1C,QAAO,MAAM,WAAW;;;AAI1B,SAAS,qBAAqB,KAAqB;AACjD,QAAO,IAAI,QAAQ,OAAO,IAAI;;;;;;;;;;;;;;AAehC,SAAgB,gBACd,MACA,gBACA,QACS;CACT,MAAM,kBAAkB,qBAAqB,aAAa,OAAO,CAAC;AAClE,KAAI,gBAEF;OADuB,eAAe,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,IACpD,WAAW,gBAAgB,CAAE,QAAO;;CAEzD,MAAM,YAAY,aAAa,OAAO;AACtC,MAAK,MAAM,SAAS,CAAC,KAAK,MAAM,KAAK,QAAQ,CAC3C,KAAI,UAAU,MAAM,SAAS,UAAU,IAAI,MAAM,SAAS,OAAO,EAAG,QAAO;AAE7E,QAAO;;AAmBT,SAAS,UAAU,MAAqC;AACtD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,KAAK,SAAS,UAAU,iBAAiB,KAAK,CAAE,QAAO;AAC3D,KAAI,KAAK,SAAS,aAAa;EAC7B,MAAM,aAAa,KAAK,SAAS;AACjC,MAAI,eAAe,UAAa,eAAe,KAAM,QAAO;AAC5D,SAAO,eAAe,cAAc,eAAe;;AAErD,QAAO;;AAGT,SAAgB,uBAAuB,MAAkC;CACvE,IAAI;AACJ,KAAI;AACF,UAAQ,aAAa,MAAM,OAAO,CAAC,MAAM,KAAK;SACxC;AACN,SAAO;GAAE,SAAS;GAAM,SAAS;GAAO,gBAAgB;GAAM;;CAGhE,IAAI,WAAiC;CACrC,IAAI,UAAyB;CAC7B,IAAI,iBAAuD;AAE3D,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,MAAM,MAAM,GAAG,MAAM;AAC3B,MAAI,CAAC,IAAK;EACV,IAAI;AACJ,MAAI;AACF,SAAM,KAAK,MAAM,IAAI;UACf;AACN;;AAEF,MAAI,aAAa,KAAM,YAAW;AAElC,MAAI,YAAY,QAAQ,IAAI,SAAS,eAAe,IAAI,SAAS,OAAO;GACtE,MAAM,QAAQ,IAAI,QAAQ;AAC1B,cACG,OAAO,MAAM,wBAAwB,IAAI,MACzC,OAAO,MAAM,4BAA4B,IAAI,MAC7C,OAAO,MAAM,aAAa,IAAI;;AAGnC,MAAI,mBAAmB,QAAQ,iBAAiB,IAAI,CAElD,kBAAiB;GAAE,KADP,IAAI,QAAQ,IAAI,aAAa,OAAO,EAAE;GAC1B,MAAM,sBAAsB,IAAI,CAAC,MAAM;GAAE;AAGnE,MAAI,YAAY,QAAQ,mBAAmB,KAAM;;AAGnD,QAAO;EAAE;EAAS,SAAS,UAAU,SAAS;EAAE;EAAgB;;;AAQlE,MAAa,0BAA0B;AAEvC,SAAgB,6BAAqC;CACnD,MAAM,EAAE,YAAY,oBAAoB;AACxC,QAAO,KAAK,cAAc,QAAQ,EAAE,aAAa;;;AAInD,SAAgB,8BACd,OAAe,4BAA4B,EAC3C,QAAQ,IACgD;AACxD,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;EAAE,MAAM;EAAG,SAAS;EAAG,WAAW,EAAE;EAAE;CACpE,MAAM,QAAQ,aAAa,MAAM,OAAO,CACrC,MAAM,KAAK,CACX,QAAQ,MAAM,EAAE,SAAS,wBAAwB,CAAC;CACrD,MAAM,OAAO,MAAM,QAAQ,MAAM,qBAAqB,KAAK,EAAE,CAAC,CAAC;AAC/D,QAAO;EAAE;EAAM,SAAS,MAAM,SAAS;EAAM,WAAW,MAAM,MAAM,CAAC,MAAM;EAAE;;AA2B/E,SAAS,cAAoC;CAC3C,MAAM,EAAE,YAAY,oBAAoB;CACxC,MAAM,SAAS,cAAc,QAAQ;AACrC,QAAO;EACL,2BAAW,IAAI,MAAM;EACFA;EACnB,yBAAyB,WAAW,8BAA8B,QAAQ,OAAO;EACjF,iBAAiB,OAAO,sBAAsB,GAAG;EACjD,UAAU,MAAM;AACd,OAAI;AACF,WAAO,SAAS,EAAE,CAAC;WACb;AACN,WAAO;;;EAGX,cAAc;EACd,WAAW,IAAI,SAASC,cAAqB,IAAI,MAAM,QAAW,EAAE,SAAS,MAAM,CAAC;EACpF,iBAAiB,2BAA2B;EAC5C,YAAY,MAAM,0BAA0B,EAAE;EAC9C,SAAS,OAAO,aAAa,4BAA4B,EAAE,yBAAyB,GAAG;EACxF;;;;;;;;AASH,eAAsB,wBACpB,QACA,YAA2C,EAAE,EACV;AACnC,KAAI,CAAC,OAAO,QAAS,QAAO,EAAE;CAE9B,MAAM,IAA0B;EAAE,GAAG,aAAa;EAAE,GAAG;EAAW;CAClE,MAAM,WAAW,MAAM,EAAE,mBAAmB;CAC5C,MAAM,QAAQ,EAAE,WAAW;CAC3B,MAAM,MAAM,EAAE,KAAK;CACnB,MAAM,EAAE,YAAY,oBAAoB;CACxC,MAAM,SAAS,cAAc,QAAQ;CACrC,MAAM,UAAoC,EAAE;CAC5C,IAAI,aAAa;AAEjB,MAAK,MAAM,KAAK,UAAU;AACxB,MAAI,EAAE,SAAS,SAAU;EACzB,MAAM,SAAS,EAAE;EAEjB,MAAM,WAAW,EAAE,uBAAuB,OAAO;AACjD,MAAI,CAAC,UAAU;AACb,KAAE,OAAO;IAAE,SAAS;IAAM,MAAM;IAAQ,QAAQ;IAAoB,CAAC;AACrE,WAAQ,KAAK;IAAE,WAAW;IAAQ,QAAQ;IAAoB,CAAC;AAC/D;;EAEF,MAAM,YAAY;EAClB,MAAM,QAAQ,EAAE,GAAI,MAAM,cAAc,YAAY,EAAG;EACvD,IAAI,UAAU;EAEd,MAAM,aAAa,EAAE,eAAe,UAAU;AAE9C,MAAI,gBAAgB,GAAG,YAAY,OAAO,EAAE;AAC1C,KAAE,OAAO;IAAE,SAAS;IAAW,MAAM;IAAQ,QAAQ;IAAkB,CAAC;AACxE,WAAQ,KAAK;IAAE;IAAW,QAAQ;IAAkB,CAAC;AACrD;;AAGF,MAAI,CAAC,YAAY;AACf,KAAE,OAAO;IAAE,SAAS;IAAW,MAAM;IAAQ,QAAQ;IAAyB,CAAC;AAC/E,WAAQ,KAAK;IAAE;IAAW,QAAQ;IAAyB,CAAC;AAC5D;;EAGF,MAAM,WAAW,EAAE,aAAa,WAAW;AAI3C,MAAI,SAAS,kBAAkB,SAAS,eAAe,QAAQ,MAAM,mBAAmB;AACtF,SAAM,oBAAoB,SAAS,eAAe;AAClD,OAAI,SAAS,eAAe,SAAS,OAAO,OAAQ,OAAM,QAAQ;AAClE,aAAU;;EAGZ,MAAM,QAAQ,EAAE,QAAQ,WAAW;EACnC,MAAM,UAAU,UAAU,OAAO,QAAQ,IAAI,SAAS,GAAG,SAAS;EAElE,MAAM,OACJ,UAAU,QAAQ,YAAY,OAC1B,aACA,UAAU,OAAO,cACf,QAAQ,QAAQ,QAAQ,EAAE,CAAC,OAC3B,CAAC,oBAAoB,KAAK,OAAO,YAAY,GAC3C,UACA,SAAS,YAAY,QAAQ,SAAS,UAAU,OAAO,mBACrD,YACA,SAAS,UACP,aACA,MAAM,SAAS,OAAO,WACpB,cACA;EAEhB,MAAM,aAAa;GACjB,SAAS;GACT,MAAM;GACN,UAAU,YAAY,OAAO,OAAO,QAAQ,QAAQ,EAAE;GACtD,SAAS,SAAS;GACnB;AAED,MAAI,MAAM;AACR,KAAE,OAAO;IAAE,GAAG;IAAY,MAAM,GAAG,MAAM,MAAM,GAAG,OAAO;IAAY,QAAQ,WAAW;IAAQ,CAAC;AACjG,WAAQ,KAAK;IAAE;IAAW,QAAQ,WAAW;IAAQ,CAAC;cAEzC,MAAM,EAAE,SAAS,QAAQ,OAAO,OAAO,EAC3C,IAAI;AACX,SAAM,SAAS;AACf,SAAM,aAAa,IAAI,aAAa;AACpC,aAAU;AACV,KAAE,OAAO;IAAE,GAAG;IAAY,MAAM,GAAG,MAAM,MAAM,GAAG,OAAO;IAAY,QAAQ;IAAQ,CAAC;AACtF,WAAQ,KAAK;IAAE;IAAW,QAAQ;IAAQ,CAAC;SACtC;AACL,KAAE,OAAO;IAAE,GAAG;IAAY,MAAM,GAAG,MAAM,MAAM,GAAG,OAAO;IAAY,QAAQ;IAAuB,CAAC;AACrG,WAAQ,KAAK;IAAE;IAAW,QAAQ;IAAuB,CAAC;;AAI9D,MAAI,SAAS;AACX,SAAM,aAAa;AACnB,gBAAa;;;AAIjB,KAAI,WAAY,GAAE,UAAU,MAAM;AAClC,QAAO"}
|
|
1
|
+
{"version":3,"file":"session-keepalive-DJA97K2X.mjs","names":["fetchLiveSessionsDefault","sendToSessionDefault"],"sources":["../src/audit/session-usage.ts","../src/daemon/session-keepalive.ts"],"sourcesContent":["/**\n * session-usage.ts — parse the usage numbers out of a Claude Code session\n * (or subagent, or pai-worker event-mirror) JSONL transcript.\n *\n * All three log shapes share the same assistant-message envelope\n * ({ type: \"assistant\", message: { id, model, usage } }), so one parser\n * covers `pai audit tokens session` and the per-log readings feeding\n * `pai audit tokens spawn`.\n *\n * Streaming writes one JSONL line per content block of the same logical\n * turn, repeating message.id and usage each time — summing every line would\n * multiply usage by the block count. Keeping only the first line seen per\n * message.id is what the reference script (and this parser) count instead.\n */\n\nimport { createReadStream, existsSync } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\nexport interface UsageTotals {\n cache_read_input_tokens: number;\n cache_creation_input_tokens: number;\n input_tokens: number;\n output_tokens: number;\n}\n\nexport interface CacheCreationSplit {\n ephemeral5m: number;\n ephemeral1h: number;\n}\n\nexport interface CompactionEvent {\n trigger: string;\n preTokens: number;\n turnIndex: number;\n}\n\nexport interface ModelSwitch {\n turnIndex: number;\n from: string;\n to: string;\n cacheRead: number;\n cacheCreation: number;\n}\n\nexport interface FallbackEvent {\n turnIndex: number;\n from: string;\n to: string;\n category: string;\n scope: string;\n}\n\nexport interface SessionUsageReport {\n path: string;\n sizeBytes: number;\n turns: number;\n totals: UsageTotals;\n /** Per-model assistant-turn counts. */\n models: Record<string, number>;\n /** cache_read + cache_creation + input on the first / last assistant turn seen. */\n firstTurnContext: number | null;\n lastTurnContext: number | null;\n cacheCreationSplit: CacheCreationSplit;\n /** Average / max of the per-turn context value across all turns; null if no turns. */\n avgContext: number | null;\n maxContext: number | null;\n /** Turns whose per-turn context value exceeds the report's threshold. */\n turnsAboveThreshold: number;\n /** Turns whose cache_creation_input_tokens exceeds 20000. */\n cacheRebuildTurns: number;\n /** Real human-authored user prompts (excludes tool-result-only \"user\" lines). */\n userPrompts: number;\n /**\n * Sum over assistant turns of the real user prompts seen before that turn;\n * multiplied by per-prompt hook tokens it gives the tokens UserPromptSubmit\n * output occupied across the whole session.\n */\n promptExposure: number;\n compactions: CompactionEvent[];\n /** ISO timestamp of the first assistant turn. */\n firstTurnAt: string | null;\n /** Model of the last folded turn; drives switch detection. */\n lastModel: string | null;\n modelSwitches: ModelSwitch[];\n fallbacks: FallbackEvent[];\n /** Timestamp (ms) of the last folded assistant turn; drives idle-gap detection. */\n lastTurnAtMs: number | null;\n /** Gaps between consecutive assistant turns exceeding 60 minutes — evidence\n * for whether the session ever went idle long enough to risk its cache TTL\n * (see sessions.cacheKeepalive, src/daemon/config.ts). */\n idleGapsOver60min: number;\n /** Real user prompts whose text is exactly the configured keepalive word\n * (set only when parseSessionUsage is called with one); null otherwise. */\n keepaliveBeats: number | null;\n}\n\nconst USAGE_KEYS: (keyof UsageTotals)[] = [\n \"cache_read_input_tokens\",\n \"cache_creation_input_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n];\n\nfunction emptyTotals(): UsageTotals {\n return { cache_read_input_tokens: 0, cache_creation_input_tokens: 0, input_tokens: 0, output_tokens: 0 };\n}\n\nexport interface AssistantLine {\n type?: string;\n uuid?: string;\n subtype?: string;\n /** Claude Code marks skill expansions and injected system reminders isMeta; they fire no UserPromptSubmit hook. */\n isMeta?: boolean;\n /** Present on newer logs: { kind: \"human\" } for a typed prompt. */\n origin?: { kind?: string };\n compactMetadata?: { trigger?: string; preTokens?: number };\n timestamp?: string;\n originalModel?: string;\n fallbackModel?: string;\n apiRefusalCategory?: string;\n scope?: string;\n message?: {\n id?: string;\n model?: string;\n usage?: Record<string, unknown> & {\n cache_creation?: { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number };\n };\n content?: string | Array<{ type?: string }>;\n stop_reason?: string | null;\n };\n}\n\n/**\n * `type:\"system\"` `subtype:\"compact_boundary\"` lines mark a compaction.\n * turnIndex is the count of assistant turns already folded when it fired,\n * so it lines up with the turn numbering the text/JSON report prints.\n */\nexport function isCompactBoundary(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"compact_boundary\";\n}\n\nexport function parseCompactionEvent(line: AssistantLine, turnIndex: number): CompactionEvent | null {\n const meta = line.compactMetadata;\n if (!meta || typeof meta.preTokens !== \"number\") return null;\n return { trigger: meta.trigger ?? \"unknown\", preTokens: meta.preTokens, turnIndex };\n}\n\n/**\n * `type:\"system\"` `subtype:\"model_refusal_fallback\"` lines mark an\n * automatic model switch fired by an API safety refusal (e.g. \"cyber\"\n * category), not a user or config choice — worth flagging separately since\n * it can rebuild the whole prompt cache mid-session.\n */\nexport function isModelFallback(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"model_refusal_fallback\";\n}\n\nexport function parseFallbackEvent(line: AssistantLine, turnIndex: number): FallbackEvent {\n return {\n turnIndex,\n from: line.originalModel ?? \"unknown\",\n to: line.fallbackModel ?? \"unknown\",\n category: line.apiRefusalCategory ?? \"unknown\",\n scope: line.scope ?? \"unknown\",\n };\n}\n\n/**\n * Fold one already-parsed JSONL line into a report being accumulated.\n * Exported separately so both the streaming file reader below and tests\n * (which build fixtures as arrays of objects, not files) share one path.\n * Returns the turn's context value when a new turn was counted, else null\n * (non-assistant line, no usage, or a duplicate message.id already seen).\n */\nexport function foldAssistantLine(\n report: Pick<\n SessionUsageReport,\n | \"turns\"\n | \"totals\"\n | \"models\"\n | \"firstTurnContext\"\n | \"lastTurnContext\"\n | \"cacheCreationSplit\"\n | \"maxContext\"\n | \"turnsAboveThreshold\"\n | \"cacheRebuildTurns\"\n | \"firstTurnAt\"\n | \"lastModel\"\n | \"modelSwitches\"\n | \"lastTurnAtMs\"\n | \"idleGapsOver60min\"\n >,\n seenIds: Set<string>,\n line: AssistantLine,\n threshold: number\n): number | null {\n if (line.type !== \"assistant\") return null;\n const message = line.message;\n const usage = message?.usage;\n if (!usage) return null;\n const id = message?.id ?? line.uuid;\n if (id) {\n if (seenIds.has(id)) return null;\n seenIds.add(id);\n }\n report.turns++;\n for (const key of USAGE_KEYS) {\n const v = usage[key];\n if (typeof v === \"number\") report.totals[key] += v;\n }\n const context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n if (report.firstTurnContext === null) {\n report.firstTurnContext = context;\n report.firstTurnAt = line.timestamp ?? null;\n }\n const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;\n if (!Number.isNaN(atMs)) {\n if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 60 * 60 * 1000) {\n report.idleGapsOver60min++;\n }\n report.lastTurnAtMs = atMs;\n }\n report.lastTurnContext = context;\n report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);\n if (context > threshold) report.turnsAboveThreshold++;\n if ((Number(usage.cache_creation_input_tokens) || 0) > 20000) report.cacheRebuildTurns++;\n const model = message?.model ?? \"unknown\";\n report.models[model] = (report.models[model] ?? 0) + 1;\n if (report.lastModel !== null && report.lastModel !== model) {\n report.modelSwitches.push({\n turnIndex: report.turns,\n from: report.lastModel,\n to: model,\n cacheRead: Number(usage.cache_read_input_tokens) || 0,\n cacheCreation: Number(usage.cache_creation_input_tokens) || 0,\n });\n }\n report.lastModel = model;\n const split = usage.cache_creation;\n if (split) {\n report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;\n report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;\n }\n return context;\n}\n\n/**\n * True for a real human-authored `type:\"user\"` prompt line: content is a\n * plain string, or a content array with no `tool_result` block. Claude Code\n * encodes tool results as `type:\"user\"` messages whose content array is\n * entirely (or partly) tool_result blocks — those must not count as prompts.\n */\nexport function isRealUserPrompt(line: AssistantLine): boolean {\n if (line.type !== \"user\") return false;\n if (line.isMeta) return false;\n if (line.origin?.kind && line.origin.kind !== \"human\") return false;\n const content = line.message?.content;\n if (typeof content === \"string\") return true;\n if (Array.isArray(content)) return !content.some((block) => block?.type === \"tool_result\");\n return false;\n}\n\n/** Plain text of a real user prompt: the string content, or its first text block. */\nexport function extractUserPromptText(line: AssistantLine): string {\n const content = line.message?.content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const block = content.find((b) => b?.type === \"text\") as { text?: string } | undefined;\n return block?.text ?? \"\";\n }\n return \"\";\n}\n\n/**\n * Parse a session/subagent/worker-event JSONL file into a usage report.\n * `keepaliveWord`, when given, counts real user prompts whose text exactly\n * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into\n * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.\n */\nexport async function parseSessionUsage(\n path: string,\n threshold = 200_000,\n keepaliveWord?: string\n): Promise<SessionUsageReport> {\n const report: SessionUsageReport = {\n path,\n sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,\n turns: 0,\n totals: emptyTotals(),\n models: {},\n firstTurnContext: null,\n lastTurnContext: null,\n cacheCreationSplit: { ephemeral5m: 0, ephemeral1h: 0 },\n avgContext: null,\n maxContext: null,\n turnsAboveThreshold: 0,\n cacheRebuildTurns: 0,\n userPrompts: 0,\n promptExposure: 0,\n compactions: [],\n firstTurnAt: null,\n lastModel: null,\n modelSwitches: [],\n fallbacks: [],\n lastTurnAtMs: null,\n idleGapsOver60min: 0,\n keepaliveBeats: keepaliveWord ? 0 : null,\n };\n const seenIds = new Set<string>();\n let contextSum = 0;\n\n const rl = createInterface({ input: createReadStream(path, \"utf8\"), crlfDelay: Infinity });\n for await (const raw of rl) {\n if (!raw.trim()) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (obj.type === \"user\") {\n if (isRealUserPrompt(obj)) {\n report.userPrompts++;\n if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) {\n report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;\n }\n }\n continue;\n }\n if (isCompactBoundary(obj)) {\n const event = parseCompactionEvent(obj, report.turns);\n if (event) report.compactions.push(event);\n continue;\n }\n if (isModelFallback(obj)) {\n report.fallbacks.push(parseFallbackEvent(obj, report.turns));\n continue;\n }\n const context = foldAssistantLine(report, seenIds, obj, threshold);\n if (context !== null) {\n contextSum += context;\n report.promptExposure += report.userPrompts;\n }\n }\n report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;\n return report;\n}\n\nexport function totalUsageTokens(totals: UsageTotals): number {\n return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;\n}\n","/**\n * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live\n * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts\n * and docs/cache-keepalive.md, \"Interactive sessions\").\n *\n * One beat = one trivial prompt typed into a session through AIBroker's\n * send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`\n * so the target sees only the bare word typed into its input line and\n * nothing is queued into its mailbox as a peer message demanding a reply.\n * A cache READ refreshes the provider's ephemeral prompt-cache TTL at\n * roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next\n * real prompt.\n *\n * Distinct from workers/keepalive.ts, which beats a *worker provider's*\n * cache on a fixed timer regardless of activity: this only beats a session\n * that has actually been idle long enough to be at risk, and only inside a\n * configured working-hours window, so it never fires while the user is\n * present at the keyboard or overnight when nobody will read the reply\n * before the cache would have expired anyway.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n fetchLiveSessions as fetchLiveSessionsDefault,\n sendToSession as sendToSessionDefault,\n type AiBrokerSessionMeta,\n} from \"../cli/lib/aibroker-client.js\";\nimport { writeJsonAtomic } from \"../config/json-store.js\";\nimport { paiHomePath } from \"../config/pai-home.js\";\nimport { appendLedger } from \"../workers/ledger.js\";\nimport { readWorkersSection } from \"../workers/config.js\";\nimport { workersLogDir } from \"../workers/paths.js\";\nimport { worktreesDir } from \"../workers/worktree.js\";\nimport { itermUuid, sessionMapPath, type SessionMapEntry } from \"../workers/scope.js\";\nimport { isRealUserPrompt, extractUserPromptText, type AssistantLine } from \"../audit/session-usage.js\";\nimport type { SessionsCacheKeepaliveConfig } from \"./config.js\";\n\n// ---------------------------------------------------------------------------\n// State file — per-session beat counters, rebuildable (see json-store.ts on\n// when NOT to use readJsonStrict: a damaged file here just resets counters,\n// never blocks the feature).\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveEntry {\n /** Beats sent since the last real (non-keepalive) user prompt. */\n beats: number;\n /** Identity of the last real user prompt observed, so a fresh one can be\n * told apart from the keepalive's own echo landing back in the transcript. */\n lastRealPromptKey: string | null;\n /** ISO stamp of the last beat sent. */\n lastBeatAt: string | null;\n}\n\nexport type SessionKeepaliveState = Record<string, SessionKeepaliveEntry>;\n\nexport function sessionKeepaliveStatePath(): string {\n return paiHomePath(\"session-keepalive.json\");\n}\n\nfunction emptyEntry(): SessionKeepaliveEntry {\n return { beats: 0, lastRealPromptKey: null, lastBeatAt: null };\n}\n\nexport function loadSessionKeepaliveState(path: string = sessionKeepaliveStatePath()): SessionKeepaliveState {\n if (!existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as SessionKeepaliveState;\n } catch {\n return {};\n }\n}\n\nexport function saveSessionKeepaliveState(\n state: SessionKeepaliveState,\n path: string = sessionKeepaliveStatePath()\n): void {\n writeJsonAtomic(path, state, { backup: false, label: path });\n}\n\n// ---------------------------------------------------------------------------\n// Active-hours window\n// ---------------------------------------------------------------------------\n\n/** Parse \"HH:MM-HH:MM\" into minutes-since-midnight. Throws on a malformed\n * spec — an explicit config error beats a window that is silently always\n * on or always off. */\nexport function parseActiveHours(spec: string): { startMin: number; endMin: number } {\n const m = spec.match(/^(\\d{1,2}):(\\d{2})-(\\d{1,2}):(\\d{2})$/);\n if (!m) {\n throw new Error(`sessions.cacheKeepalive.activeHours: invalid \"${spec}\" (want \"HH:MM-HH:MM\")`);\n }\n return {\n startMin: Number(m[1]) * 60 + Number(m[2]),\n endMin: Number(m[3]) * 60 + Number(m[4]),\n };\n}\n\n/**\n * Whether `now` (local time) sits inside the window. A window that wraps\n * midnight (e.g. \"22:00-06:00\") is honoured by inverting the test instead of\n * requiring startMin < endMin.\n */\nexport function isWithinActiveHours(now: Date, spec: string): boolean {\n const { startMin, endMin } = parseActiveHours(spec);\n const nowMin = now.getHours() * 60 + now.getMinutes();\n if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;\n return nowMin >= startMin || nowMin < endMin;\n}\n\n// ---------------------------------------------------------------------------\n// Transcript lookup\n// ---------------------------------------------------------------------------\n\n/**\n * Full path to a live session's transcript under ~/.claude/projects, or null\n * when none is found — the case for a session too new to have written a file\n * yet. A worker running in a worktree DOES write a transcript here (Claude\n * Code encodes its worktree cwd as the project dir name), so workers are not\n * excluded \"for free\" — see isWorkerSession.\n */\nexport function findSessionTranscript(\n sessionId: string,\n projectsDir: string = join(homedir(), \".claude\", \"projects\")\n): string | null {\n let projectDirs: string[];\n try {\n projectDirs = readdirSync(projectsDir);\n } catch {\n return null;\n }\n for (const projectDir of projectDirs) {\n const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id\n * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`\n * transcripts are named after — the two are unrelated identifiers. The\n * status line bridges them on every refresh via `claude-session-map.json`\n * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the\n * map entries whose `term` pane UUID matches, the most recently written one.\n * Returns null when the pane has no (fresh enough) mapped Claude session.\n */\nexport function resolveClaudeSessionIdFromMap(paneId: string, logDir: string): string | null {\n const path = sessionMapPath(logDir);\n if (!paneId || !existsSync(path)) return null;\n let map: Record<string, SessionMapEntry>;\n try {\n map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n } catch {\n return null;\n }\n let best: SessionMapEntry | null = null;\n for (const entry of Object.values(map)) {\n if (!entry.term || itermUuid(entry.term) !== paneId) continue;\n if (!best || entry.ts > best.ts) best = entry;\n }\n return best?.session ?? null;\n}\n\n/** Claude Code's project-dir encoding of a cwd: every \"/\" becomes \"-\". */\nfunction encodeProjectDirName(cwd: string): string {\n return cwd.replace(/\\//g, \"-\");\n}\n\n/**\n * Is this live \"claude\"-kind session actually a worker pane rather than an\n * interactive one? `claude -p` workers running in a worktree write their\n * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),\n * so a missing transcript is NOT how workers get excluded — this predicate\n * is. Either signal is enough:\n * (a) the transcript's project-dir name is the encoded form of a path\n * under <logDir>/worktrees (every worker worktree lives there), or\n * (b) the broker's session name/paiName names a path under the workers\n * log dir or one of its worktrees (best-effort: AIBroker does not\n * guarantee this, but honors it when present).\n */\nexport function isWorkerSession(\n meta: AiBrokerSessionMeta,\n transcriptPath: string | null,\n logDir: string\n): boolean {\n const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));\n if (transcriptPath) {\n const projectDirName = transcriptPath.split(\"/\").slice(0, -1).pop() ?? \"\";\n if (projectDirName.startsWith(worktreesPrefix)) return true;\n }\n const rawPrefix = worktreesDir(logDir);\n for (const field of [meta.name, meta.paiName]) {\n if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Tail snapshot: last turn's context, mid-turn flag, last real prompt\n// ---------------------------------------------------------------------------\n\nexport interface TranscriptSnapshot {\n /** cache_read + cache_creation + input on the most recent assistant turn seen. */\n context: number | null;\n /** True when the last line in the file shows the session still working: an\n * assistant turn with no usage yet (streaming) or a stop_reason other than\n * end_turn/stop_sequence (e.g. mid tool-call), or a real user prompt with\n * no reply behind it yet. A beat sent now would land mid-generation. */\n midTurn: boolean;\n /** The last real (human-authored) user prompt seen, if any. */\n lastRealPrompt: { key: string; text: string } | null;\n}\n\nfunction isMidTurn(line: AssistantLine | null): boolean {\n if (!line) return false;\n if (line.type === \"user\" && isRealUserPrompt(line)) return true;\n if (line.type === \"assistant\") {\n const stopReason = line.message?.stop_reason;\n if (stopReason === undefined || stopReason === null) return true;\n return stopReason !== \"end_turn\" && stopReason !== \"stop_sequence\";\n }\n return false;\n}\n\nexport function readTranscriptSnapshot(path: string): TranscriptSnapshot {\n let lines: string[];\n try {\n lines = readFileSync(path, \"utf8\").split(\"\\n\");\n } catch {\n return { context: null, midTurn: false, lastRealPrompt: null };\n }\n\n let lastLine: AssistantLine | null = null;\n let context: number | null = null;\n let lastRealPrompt: { key: string; text: string } | null = null;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const raw = lines[i].trim();\n if (!raw) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (lastLine === null) lastLine = obj;\n\n if (context === null && obj.type === \"assistant\" && obj.message?.usage) {\n const usage = obj.message.usage;\n context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n }\n\n if (lastRealPrompt === null && isRealUserPrompt(obj)) {\n const key = obj.uuid ?? obj.timestamp ?? String(i);\n lastRealPrompt = { key, text: extractUserPromptText(obj).trim() };\n }\n\n if (context !== null && lastRealPrompt !== null) break;\n }\n\n return { context, midTurn: isMidTurn(lastLine), lastRealPrompt };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger — same file WORKER-KEEPALIVE lines go to (workers.logDir/ledger.log)\n// ---------------------------------------------------------------------------\n\n/** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */\nexport const SESSION_KEEPALIVE_EVENT = \"SESSION-KEEPALIVE\";\n\nexport function sessionKeepaliveLedgerPath(): string {\n const { workers } = readWorkersSection();\n return join(workersLogDir(workers), \"ledger.log\");\n}\n\n/** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */\nexport function sessionKeepaliveLedgerSummary(\n path: string = sessionKeepaliveLedgerPath(),\n lastN = 10\n): { sent: number; skipped: number; lastLines: string[] } {\n if (!existsSync(path)) return { sent: 0, skipped: 0, lastLines: [] };\n const lines = readFileSync(path, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));\n const sent = lines.filter((l) => / result=sent(\\s|$)/.test(l)).length;\n return { sent, skipped: lines.length - sent, lastLines: lines.slice(-lastN) };\n}\n\n// ---------------------------------------------------------------------------\n// The tick\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveDeps {\n now: () => Date;\n fetchLiveSessions: () => Promise<AiBrokerSessionMeta[]>;\n /** AIBroker pane id (fetchLiveSessions' `sessionId`) -> Claude transcript\n * session id, via the status line's claude-session-map.json bridge. */\n resolveClaudeSessionId: (paneId: string) => string | null;\n findTranscript: (sessionId: string) => string | null;\n mtimeMs: (path: string) => number | null;\n readSnapshot: (path: string) => TranscriptSnapshot;\n sendBeat: (sessionId: string, text: string) => Promise<{ ok: boolean; error?: string }>;\n loadState: () => SessionKeepaliveState;\n saveState: (s: SessionKeepaliveState) => void;\n ledger: (kv: Record<string, string | number | null | undefined>) => void;\n}\n\nexport interface SessionKeepaliveResult {\n sessionId: string;\n result: \"sent\" | string; // \"skipped:<reason>\"\n}\n\nfunction defaultDeps(): SessionKeepaliveDeps {\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n return {\n now: () => new Date(),\n fetchLiveSessions: fetchLiveSessionsDefault,\n resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),\n findTranscript: (id) => findSessionTranscript(id),\n mtimeMs: (p) => {\n try {\n return statSync(p).mtimeMs;\n } catch {\n return null;\n }\n },\n readSnapshot: readTranscriptSnapshot,\n sendBeat: (id, text) => sendToSessionDefault(id, text, undefined, { noReply: true }),\n loadState: () => loadSessionKeepaliveState(),\n saveState: (s) => saveSessionKeepaliveState(s),\n ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv),\n };\n}\n\n/**\n * One tick over every live interactive session: beat the ones that qualify,\n * skip (with a reason) the ones that don't. Returns one result per live\n * \"claude\"-kind session for tests to assert against; writes the ledger line\n * and (when any counter changed) the state file as side effects.\n */\nexport async function runSessionKeepaliveTick(\n config: SessionsCacheKeepaliveConfig,\n overrides: Partial<SessionKeepaliveDeps> = {}\n): Promise<SessionKeepaliveResult[]> {\n if (!config.enabled) return [];\n\n const d: SessionKeepaliveDeps = { ...defaultDeps(), ...overrides };\n const sessions = await d.fetchLiveSessions();\n const state = d.loadState();\n const now = d.now();\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n const results: SessionKeepaliveResult[] = [];\n let anyChanged = false;\n\n for (const s of sessions) {\n if (s.kind !== \"claude\") continue;\n const paneId = s.sessionId;\n\n const claudeId = d.resolveClaudeSessionId(paneId);\n if (!claudeId) {\n d.ledger({ session: null, pane: paneId, result: \"skipped:unmapped\" });\n results.push({ sessionId: paneId, result: \"skipped:unmapped\" });\n continue;\n }\n const sessionId = claudeId;\n const entry = { ...(state[sessionId] ?? emptyEntry()) };\n let changed = false;\n\n const transcript = d.findTranscript(sessionId);\n\n if (isWorkerSession(s, transcript, logDir)) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:worker\" });\n results.push({ sessionId, result: \"skipped:worker\" });\n continue;\n }\n\n if (!transcript) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:no-transcript\" });\n results.push({ sessionId, result: \"skipped:no-transcript\" });\n continue;\n }\n\n const snapshot = d.readSnapshot(transcript);\n\n // A fresh real prompt (not the keepalive's own echo) resets the beat\n // count — the idle stretch it capped is over.\n if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {\n entry.lastRealPromptKey = snapshot.lastRealPrompt.key;\n if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;\n changed = true;\n }\n\n const mtime = d.mtimeMs(transcript);\n const idleMin = mtime === null ? null : (now.getTime() - mtime) / 60_000;\n\n const skip: string | null =\n mtime === null || idleMin === null\n ? \"no-mtime\"\n : idleMin < config.idleMinutes\n ? `idle:${idleMin.toFixed(1)}min`\n : !isWithinActiveHours(now, config.activeHours)\n ? \"hours\"\n : snapshot.context === null || snapshot.context < config.minContextTokens\n ? \"context\"\n : snapshot.midTurn\n ? \"mid-turn\"\n : entry.beats >= config.maxBeats\n ? \"max-beats\"\n : null;\n\n const ledgerBase = {\n session: sessionId,\n pane: paneId,\n idle_min: idleMin === null ? null : idleMin.toFixed(1),\n context: snapshot.context,\n };\n\n if (skip) {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: `skipped:${skip}` });\n results.push({ sessionId, result: `skipped:${skip}` });\n } else {\n const sent = await d.sendBeat(paneId, config.prompt);\n if (sent.ok) {\n entry.beats += 1;\n entry.lastBeatAt = now.toISOString();\n changed = true;\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"sent\" });\n results.push({ sessionId, result: \"sent\" });\n } else {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"skipped:send-failed\" });\n results.push({ sessionId, result: \"skipped:send-failed\" });\n }\n }\n\n if (changed) {\n state[sessionId] = entry;\n anyChanged = true;\n }\n }\n\n if (anyChanged) d.saveState(state);\n return results;\n}\n\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as parseSessionTitleChunk, c as yieldToEventLoop, f as sha256File, i as isPathTooBroadForContentScan, l as chunkMarkdown, n as chunkId, o as walkContentFiles, r as detectTier, s as walkMdFiles, t as INDEX_YIELD_EVERY } from "./helpers-
|
|
1
|
+
import { a as parseSessionTitleChunk, c as yieldToEventLoop, f as sha256File, i as isPathTooBroadForContentScan, l as chunkMarkdown, n as chunkId, o as walkContentFiles, r as detectTier, s as walkMdFiles, t as INDEX_YIELD_EVERY } from "./helpers-Dj-B314s.mjs";
|
|
2
2
|
import { t as STOP_WORDS } from "./stop-words-ClCwO8mZ.mjs";
|
|
3
3
|
import { n as buildFtsQuery, r as isQuerySyntaxError } from "./search-BaTbyWjK.mjs";
|
|
4
4
|
import { n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DfrYNIng.mjs";
|
|
@@ -932,4 +932,4 @@ var SQLiteBackend = class {
|
|
|
932
932
|
|
|
933
933
|
//#endregion
|
|
934
934
|
export { SQLiteBackend };
|
|
935
|
-
//# sourceMappingURL=sqlite-
|
|
935
|
+
//# sourceMappingURL=sqlite-CfKfuggn.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-D9p5a4qe.mjs","names":[],"sources":["../src/storage/sqlite/indexer.ts","../src/storage/sqlite/tunnels.ts","../src/storage/sqlite/sources.ts","../src/storage/sqlite/search.ts","../src/storage/sqlite/kg-entity.ts","../src/storage/sqlite.ts"],"sourcesContent":["/**\n * Synchronous (SQLite) indexer — moved from memory/indexer/sync.ts so the\n * only file that opens prepared statements against the federation database\n * lives under src/storage/ (design doc docs/design/postgres-only.md, unit 7).\n *\n * Scans project memory/ and Notes/ directories, chunks markdown files, and\n * inserts the resulting chunks into the federation memory store for BM25\n * search. Uses raw better-sqlite3 Database directly for maximum SQLite\n * performance (synchronous transactions, no serialisation overhead) — the\n * reason SQLiteBackend keeps this path instead of routing through the\n * generic (StorageBackend) indexer that Postgres uses.\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport { chunkMarkdown } from \"../../memory/chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"../../memory/indexer/helpers.js\";\nimport type { IndexResult } from \"../../memory/indexer/types.js\";\n\n// ---------------------------------------------------------------------------\n// Single-file indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file into the federation database.\n *\n * @returns true if the file was re-indexed (changed or new), false if skipped.\n */\nfunction indexFile(\n db: Database,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): boolean {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n // File unreadable or missing — skip silently\n return false;\n }\n\n const hash = sha256File(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n const existing = db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, relativePath) as { hash: string } | undefined;\n\n if (existing?.hash === hash) {\n return false;\n }\n\n const oldChunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, relativePath) as Array<{ id: string }>;\n\n const deleteFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunk = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const row of oldChunkIds) {\n deleteFts.run(row.id);\n }\n deleteChunk.run(projectId, relativePath);\n })();\n\n const chunks = chunkMarkdown(content);\n\n const insertChunk = db.prepare(`\n INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertFts = db.prepare(`\n INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const upsertFile = db.prepare(`\n INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size\n `);\n\n const updatedAt = Date.now();\n\n db.transaction(() => {\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(projectId, relativePath, i, chunk.startLine, chunk.endLine);\n insertChunk.run(\n id, projectId, source, tier, relativePath,\n chunk.startLine, chunk.endLine, chunk.hash, chunk.text, updatedAt,\n );\n insertFts.run(\n chunk.text, id, projectId, relativePath, source, tier,\n chunk.startLine, chunk.endLine,\n );\n }\n upsertFile.run(projectId, relativePath, source, tier, hash, mtime, size);\n })();\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index all memory, Notes, and content files for a single registered project.\n */\nasync function indexProject(\n db: Database,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files with the standard filename format:\n // \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n {\n const titleInsertChunk = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n {\n const updatedAt = Date.now();\n const titleInsertChunk2 = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts2 = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk2.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts2.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({\n absPath: claudeMemoryMd,\n rootBase: claudeProjectDir,\n source: \"memory\",\n tier: \"evergreen\",\n });\n }\n\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = indexFile(db, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const count = db\n .prepare(\"SELECT COUNT(*) as n FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .get(projectId, relPath) as { n: number };\n\n result.filesProcessed++;\n result.chunksCreated += count.n;\n } else {\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths: remove DB entries for files that no longer exist on disk.\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n\n const stalePaths: string[] = [];\n for (const row of dbChunkPaths) {\n const basePath = row.path.endsWith(\"::title\")\n ? row.path.slice(0, -\"::title\".length)\n : row.path;\n if (!livePaths.has(basePath)) {\n stalePaths.push(row.path);\n }\n }\n\n if (stalePaths.length > 0) {\n const deleteChunksFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n const deleteFile = db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const stalePath of stalePaths) {\n const chunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, stalePath) as Array<{ id: string }>;\n for (const { id } of chunkIds) {\n deleteChunksFts.run(id);\n }\n deleteChunks.run(projectId, stalePath);\n deleteFile.run(projectId, stalePath);\n }\n })();\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing (all registered projects)\n// ---------------------------------------------------------------------------\n\n/**\n * Index all active projects registered in the registry DB.\n *\n * Async: yields to the event loop between each project so that the daemon's\n * Unix socket server can process IPC requests (e.g. status) while indexing.\n */\nexport async function indexAllSqlite(\n db: Database,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProject(db, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n","/**\n * SQLite implementation of cross-project concept \"tunnel\" detection — moved\n * from memory/tunnels.ts so the only file that runs raw FTS5/vocab SQL\n * against the federation database lives under src/storage/.\n *\n * Algorithm: pull candidate terms from the FTS5 vocabulary shadow table (or\n * sample chunk text when the vocab view is unavailable), then for each\n * candidate count distinct projects and total occurrences.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { STOP_WORDS } from \"../../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport type { Tunnel, FindTunnelsOptions, FindTunnelsResult } from \"../../memory/tunnels.js\";\n\ninterface ProjectSlugMap {\n [id: number]: string;\n}\n\nasync function findTunnelsForSlugMap(\n db: Database,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>,\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n let candidateTerms: string[] = [];\n\n try {\n const vocabRows = db\n .prepare(\n `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n FROM memory_fts_v\n GROUP BY term\n HAVING SUM(cnt) >= ?\n ORDER BY SUM(doc) DESC\n LIMIT 500`\n )\n .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n candidateTerms = vocabRows\n .map((r) => r.term)\n .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n } catch {\n // Vocab table not available — fall back to sampling common words from chunks.\n const sampleRows = db\n .prepare(\n `SELECT LOWER(text) AS text FROM memory_chunks\n WHERE LENGTH(text) > 20\n ORDER BY RANDOM()\n LIMIT 2000`\n )\n .all() as Array<{ text: string }>;\n\n const freq = new Map<string, number>();\n for (const { text } of sampleRows) {\n const tokens = text\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n for (const t of tokens) {\n freq.set(t, (freq.get(t) ?? 0) + 1);\n }\n }\n candidateTerms = [...freq.entries()]\n .filter(([, n]) => n >= opts.min_occurrences)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 200)\n .map(([t]) => t);\n }\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 2 — for each candidate, count distinct projects and occurrences.\n const tunnels: Tunnel[] = [];\n\n for (const term of candidateTerms) {\n try {\n const rows = db\n .prepare(\n `SELECT c.project_id, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM memory_fts f\n JOIN memory_chunks c ON c.id = f.id\n WHERE memory_fts MATCH ?\n AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n GROUP BY c.project_id`\n )\n .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n project_id: number;\n cnt: number;\n first_seen: number;\n last_seen: number;\n }>;\n\n if (rows.length < opts.min_projects) continue;\n\n const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n if (totalOccurrences < opts.min_occurrences) continue;\n\n const projects = rows\n .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n .filter(Boolean);\n const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n tunnels.push({\n concept: term,\n projects,\n occurrences: totalOccurrences,\n first_seen: firstSeen,\n last_seen: lastSeen,\n });\n } catch {\n // Skip problematic terms\n continue;\n }\n }\n\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n/** Find cross-project concept tunnels against a SQLite federation database. */\nexport async function findTunnelsSqlite(\n db: Database,\n registry: RegistryBackend,\n options?: FindTunnelsOptions,\n): Promise<FindTunnelsResult> {\n const opts: Required<FindTunnelsOptions> = {\n min_projects: options?.min_projects ?? 2,\n min_occurrences: options?.min_occurrences ?? 3,\n limit: options?.limit ?? 20,\n };\n\n const projectRows = await registry.listProjects({ excludeArchived: true });\n const slugMap: ProjectSlugMap = {};\n for (const { id, slug } of projectRows) {\n slugMap[id] = slug;\n }\n\n return findTunnelsForSlugMap(db, slugMap, opts);\n}\n","/**\n * SQLite implementation of the `pai memory sources` aggregate report — moved\n * from cli/commands/memory/sources.ts so the only file running ad-hoc\n * aggregate SQL against the federation database lives under src/storage/.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { MemorySourcesReport } from \"../interface.js\";\n\nexport function getMemorySourcesReportSqlite(db: Database): MemorySourcesReport {\n const composition = db\n .prepare(\n `SELECT source, tier, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY source, tier ORDER BY COUNT(*) DESC`\n )\n .all() as Array<{ source: string; tier: string; chunks: number; embedded: number }>;\n\n const paths = db\n .prepare(`SELECT path, COUNT(*) AS chunks FROM memory_chunks GROUP BY path`)\n .all() as Array<{ path: string; chunks: number }>;\n\n const churn = db\n .prepare(\n `SELECT date(updated_at/1000,'unixepoch') AS day, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY day ORDER BY day DESC LIMIT 10`\n )\n .all() as Array<{ day: string; chunks: number; embedded: number }>;\n\n return { composition, paths, churn };\n}\n","/**\n * SQLite search implementation — moved from memory/search.ts so the only\n * file running raw FTS5/vector queries against the federation database\n * lives under src/storage/. Query building, slug population, and recency\n * boosting are pure logic with no SQL and stay in memory/search.ts.\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via the indexer's embed pass.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../../memory/embeddings.js\";\nimport {\n buildFtsQuery,\n isQuerySyntaxError,\n type SearchResult,\n type SearchOptions,\n} from \"../../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Keyword (BM25) search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n const sql = `\n SELECT\n c.id,\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n c.updated_at,\n c.last_accessed_at,\n c.relevance_score,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch (e) {\n // FTS5 MATCH throws on a malformed query, and for THAT an empty result is the\n // honest answer — nothing matches a query that cannot be parsed.\n //\n // Everything else is a failure of the store: a missing table, a corrupt\n // index, a locked database. Those used to return [] as well, which made an\n // unusable index byte-identical to a genuine miss. The Postgres path had the\n // same defect and it cost a real wrong answer on 2026-08-04 — the backend was\n // down for two hours, every search reported \"No results found\", and a sibling\n // session told the owner a DMARC note did not exist. See\n // storage/postgres/search.ts.\n if (!isQuerySyntaxError(e)) {\n throw new Error(\n `Memory keyword search failed — the index is unusable, so this is NOT an ` +\n `empty result set. Cause: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => {\n const baseScore = -row.bm25_score;\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n })\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered.\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding, updated_at, last_accessed_at, relevance_score\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n }>;\n\n if (rows.length === 0) return [];\n\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const baseScore = cosineSimilarity(queryEmbedding, vec);\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n });\n\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n const keywordResults = searchMemory(db, query, { ...opts, maxResults: 50 });\n const semanticResults = searchMemorySemantic(db, queryEmbedding, { ...opts, maxResults: 50 });\n\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 = kw * kwScore + sw * 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// ---------------------------------------------------------------------------\n// Access timestamp tracking (QW2)\n// ---------------------------------------------------------------------------\n\n/**\n * Update last_accessed_at for a set of chunk IDs to the current timestamp.\n *\n * Best-effort: errors are silently ignored so search is never blocked.\n */\nexport function touchChunksLastAccessed(db: Database, chunkIds: string[]): void {\n if (chunkIds.length === 0) return;\n try {\n const now = Date.now();\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n db.prepare(\n `UPDATE memory_chunks SET last_accessed_at = ? WHERE id IN (${placeholders})`\n ).run(now, ...chunkIds);\n } catch {\n // non-critical — do not block search results\n }\n}\n","/**\n * SQLite kg_entities CRUD — moved from memory/kg-entity.ts so the only file\n * running raw SQL against kg_entities lives under src/storage/. The pure\n * content-addressing helpers (entityContentId/edgeContentId) and the\n * KgEntity/KgEntityUpsertParams types they share with PostgresBackend stay\n * in memory/kg-entity.ts.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { entityContentId, type KgEntity, type KgEntityUpsertParams } from \"../../memory/kg-entity.js\";\n\n/**\n * Upsert a KG entity in the federation SQLite database.\n *\n * If the entity already exists for this tenant:\n * - Updates last_seen to now\n * - Increments mention_count\n * - Updates description if provided (overwrites older description)\n *\n * Returns the entity_id for use as a foreign key in kg_triples.\n */\nexport function upsertKgEntitySqlite(\n db: Database,\n params: KgEntityUpsertParams\n): string {\n const tenantId = params.tenantId ?? \"default\";\n const entityId = entityContentId(params.name, tenantId);\n const now = Date.now();\n\n db.prepare(`\n INSERT INTO kg_entities\n (entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)\n VALUES\n (?, ?, ?, ?, ?, ?, ?, 1, 0.5)\n ON CONFLICT(entity_id) DO UPDATE SET\n last_seen = excluded.last_seen,\n mention_count = mention_count + 1,\n description = COALESCE(excluded.description, description),\n type = CASE WHEN excluded.type != 'unknown' THEN excluded.type ELSE type END\n `).run(\n entityId,\n tenantId,\n params.name,\n params.type ?? \"unknown\",\n params.description ?? null,\n now,\n now\n );\n\n return entityId;\n}\n\n/**\n * Look up a KG entity by name within a tenant.\n * Returns null if the entity does not exist.\n */\nexport function findKgEntitySqlite(\n db: Database,\n name: string,\n tenantId = \"default\"\n): KgEntity | null {\n const entityId = entityContentId(name, tenantId);\n const row = db.prepare(\n \"SELECT * FROM kg_entities WHERE entity_id = ? AND tenant_id = ?\"\n ).get(entityId, tenantId) as KgEntity | undefined;\n return row ?? null;\n}\n\n/**\n * List KG entities for a tenant, optionally filtered by type.\n */\nexport function listKgEntitiesSqlite(\n db: Database,\n tenantId = \"default\",\n type?: string,\n limit = 100\n): KgEntity[] {\n if (type) {\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? AND type = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, type, limit) as KgEntity[];\n }\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, limit) as KgEntity[];\n}\n\n/**\n * Apply an EMA (Exponential Moving Average) feedback update to an entity's weight.\n *\n * EMA formula: new_weight = old_weight + alpha * (target - old_weight)\n */\nexport function updateEntityFeedbackWeightSqlite(\n db: Database,\n entityId: string,\n normalizedRating: number,\n alpha = 0.1\n): void {\n const row = db.prepare(\n \"SELECT feedback_weight FROM kg_entities WHERE entity_id = ?\"\n ).get(entityId) as { feedback_weight: number } | undefined;\n\n if (!row) return;\n\n const newWeight = row.feedback_weight + alpha * (normalizedRating - row.feedback_weight);\n db.prepare(\n \"UPDATE kg_entities SET feedback_weight = ? WHERE entity_id = ?\"\n ).run(newWeight, entityId);\n}\n","/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type {\n StorageBackend, ChunkRow, FileRow, FederationStats,\n MemorySourcesReport, FindTunnelsOptions, FindTunnelsResult,\n KgStats, ObservationRow, ObservationWithCwd, StoreObservationInput, QueryObservationsOptions,\n ObservationStats, SessionSummaryRow, StoreSessionSummaryInput,\n SkillTelemetryRow, RecordSkillInvocationInput, QuerySkillTelemetryOptions,\n} from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport type { KgEntity, KgEntityUpsertParams } from \"../memory/kg-entity.js\";\nimport type { KgAddParams, KgQueryParams, KgTriple, KgContradiction } from \"../memory/kg.js\";\nimport type { IndexResult } from \"../memory/indexer/types.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { indexAllSqlite } from \"./sqlite/indexer.js\";\nimport { findTunnelsSqlite } from \"./sqlite/tunnels.js\";\nimport { getMemorySourcesReportSqlite } from \"./sqlite/sources.js\";\nimport { searchMemory, searchMemorySemantic, touchChunksLastAccessed } from \"./sqlite/search.js\";\nimport {\n upsertKgEntitySqlite,\n findKgEntitySqlite,\n listKgEntitiesSqlite,\n updateEntityFeedbackWeightSqlite,\n} from \"./sqlite/kg-entity.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n readonly supportsPostgresFeatures = false;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle. Storage-internal only —\n * used by db-admin.ts for `pai db query|tables|schema` (the one place\n * outside this class allowed to reach for it). Every other operation must\n * be a StorageBackend method; nothing outside src/storage/ may call this.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n async getProjectStats(projectId: number): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n return rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n const deleteFile = this.db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const path of paths) {\n const ids = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n for (const { id } of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n deleteFile.run(projectId, path);\n }\n })();\n }\n\n async getUnembeddedChunkIds(\n projectId?: number,\n limit?: number,\n after?: { projectId: number; id: string } | null,\n ): Promise<Array<{ id: string; text: string; project_id: number; path: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n // Once paginating, the priority ORDER BY below no longer applies — pages\n // must be ordered by the same (project_id, id) the cursor seeks on.\n const paginating = after !== undefined;\n if (after) {\n conditions.push(\"(project_id > ? OR (project_id = ? AND id > ?))\");\n params.push(after.projectId, after.projectId, after.id);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n // Prioritize real knowledge notes over PAI session/job-search noise.\n // CASE expression assigns lower priority numbers to knowledge paths.\n const orderBy = paginating\n ? \"project_id, id\"\n : `CASE\n WHEN path LIKE '🧠 Ideaverse/%' THEN 0\n WHEN path LIKE 'Z - Zettelkasten/%' THEN 0\n WHEN path LIKE '💼 Business/%' THEN 0\n WHEN path LIKE '📆 Meetings/%' THEN 1\n WHEN path LIKE '💡 Insights/%' THEN 1\n WHEN path LIKE '👨💻 People/%' THEN 1\n WHEN path LIKE 'University/%' THEN 1\n WHEN path LIKE 'Copilot/%' THEN 1\n WHEN path LIKE '🗓️ Daily Notes/%' THEN 2\n WHEN path LIKE 'PAI/%' THEN 3\n WHEN path LIKE '09-job-search/%' THEN 4\n WHEN path LIKE 'seriousletter/%' THEN 4\n WHEN path LIKE 'Attachments/%' THEN 5\n ELSE 2\n END, id`;\n const rows = this.db\n .prepare(`SELECT id, text, project_id, path FROM memory_chunks ${where}\n ORDER BY ${orderBy}${limit !== undefined ? \" LIMIT ?\" : \"\"}`)\n .all(...params, ...(limit !== undefined ? [limit] : [])) as Array<{ id: string; text: string; project_id: number; path: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n\n // -------------------------------------------------------------------------\n // Vault operations — not supported on SQLite backend (use Postgres)\n // -------------------------------------------------------------------------\n\n private vaultNotSupported(): never {\n throw new Error(\"Vault operations require the Postgres backend\");\n }\n\n async upsertVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async getVaultFile(): Promise<null> { this.vaultNotSupported(); }\n async getVaultFileByInode(): Promise<null> { this.vaultNotSupported(); }\n async getAllVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async getRecentVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFiles(): Promise<number> { this.vaultNotSupported(); }\n async upsertVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async replaceLinksForSources(): Promise<void> { this.vaultNotSupported(); }\n async getLinksFromSource(): Promise<never[]> { this.vaultNotSupported(); }\n async getLinksToTarget(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkGraph(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertVaultHealth(): Promise<void> { this.vaultNotSupported(); }\n async getVaultHealth(): Promise<null> { this.vaultNotSupported(); }\n async getOrphans(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinks(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async replaceNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async resolveVaultName(): Promise<never[]> { this.vaultNotSupported(); }\n async searchVaultNameIndex(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinksFromPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksWithEmbeddings(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksForPath(): Promise<never[]> { this.vaultNotSupported(); }\n async searchChunksByText(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFilesWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultFilesAfter(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksAfter(): Promise<number> { this.vaultNotSupported(); }\n async getDeadLinksWithLineNumbers(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivity(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getAllVaultFilePaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdges(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultAlias(): Promise<null> { this.vaultNotSupported(); }\n\n // -------------------------------------------------------------------------\n // Knowledge-graph entities (kg_entities) — wraps storage/sqlite/kg-entity.ts\n // -------------------------------------------------------------------------\n\n async upsertKgEntity(params: KgEntityUpsertParams): Promise<string> {\n return upsertKgEntitySqlite(this.db, params);\n }\n\n async findKgEntity(name: string, tenantId = \"default\"): Promise<KgEntity | null> {\n return findKgEntitySqlite(this.db, name, tenantId);\n }\n\n async listKgEntities(tenantId = \"default\", type?: string, limit = 100): Promise<KgEntity[]> {\n return listKgEntitiesSqlite(this.db, tenantId, type, limit);\n }\n\n async updateEntityFeedbackWeight(entityId: string, normalizedRating: number, alpha = 0.1): Promise<void> {\n return updateEntityFeedbackWeightSqlite(this.db, entityId, normalizedRating, alpha);\n }\n\n // -------------------------------------------------------------------------\n // Chunk feedback / access tracking\n // -------------------------------------------------------------------------\n\n async getChunksForFeedback(chunkIds: string[]): Promise<Array<{ id: string; text: string; relevanceScore: number | null }>> {\n if (chunkIds.length === 0) return [];\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(`SELECT id, text, relevance_score FROM memory_chunks WHERE id IN (${placeholders})`)\n .all(...chunkIds) as Array<{ id: string; text: string; relevance_score: number | null }>;\n return rows.map((r) => ({ id: r.id, text: r.text, relevanceScore: r.relevance_score }));\n }\n\n async updateChunkRelevanceScore(chunkId: string, score: number): Promise<void> {\n this.db.prepare(\"UPDATE memory_chunks SET relevance_score = ? WHERE id = ?\").run(score, chunkId);\n }\n\n async touchChunksLastAccessed(chunkIds: string[]): Promise<void> {\n touchChunksLastAccessed(this.db, chunkIds);\n }\n\n // -------------------------------------------------------------------------\n // Global indexing\n // -------------------------------------------------------------------------\n\n async indexAll(registry: RegistryBackend): Promise<{ projects: number; result: IndexResult }> {\n return indexAllSqlite(this.db, registry);\n }\n\n // -------------------------------------------------------------------------\n // Cross-project concept tunnels\n // -------------------------------------------------------------------------\n\n async findTunnels(registry: RegistryBackend, options?: FindTunnelsOptions): Promise<FindTunnelsResult> {\n return findTunnelsSqlite(this.db, registry, options);\n }\n\n // -------------------------------------------------------------------------\n // Memory sources report\n // -------------------------------------------------------------------------\n\n async getMemorySourcesReport(): Promise<MemorySourcesReport> {\n return getMemorySourcesReportSqlite(this.db);\n }\n\n // -------------------------------------------------------------------------\n // Temporal knowledge graph / observations / skill telemetry — not\n // supported on SQLite (kg_triples, pai_observations, pai_session_summaries,\n // pai_skill_telemetry only exist in the Postgres schema).\n // -------------------------------------------------------------------------\n\n private postgresOnly(feature: string): never {\n throw new Error(`${feature} is not supported on the sqlite backend — set \"storageBackend\": \"postgres\" in the daemon config`);\n }\n\n async addKgTriple(): Promise<KgTriple> { this.postgresOnly(\"kg_triples\"); }\n async queryKgTriples(): Promise<KgTriple[]> { this.postgresOnly(\"kg_triples\"); }\n async invalidateKgTriple(): Promise<void> { this.postgresOnly(\"kg_triples\"); }\n async getKgContradictions(): Promise<KgContradiction[]> { this.postgresOnly(\"kg_triples\"); }\n async getKgStats(): Promise<KgStats> { this.postgresOnly(\"kg_triples\"); }\n\n async storeObservation(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async storeObservationWithProject(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async queryObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async queryRecentObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async querySessionObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async getObservationStats(): Promise<ObservationStats> { this.postgresOnly(\"observations\"); }\n async getObservationTypesForPaths(): Promise<Map<string, Record<string, number>>> { return new Map(); }\n async storeSessionSummary(): Promise<void> { this.postgresOnly(\"session summaries\"); }\n async queryRecentSummaries(): Promise<SessionSummaryRow[]> { this.postgresOnly(\"session summaries\"); }\n async recordSkillInvocation(): Promise<void> { this.postgresOnly(\"skill telemetry\"); }\n async querySkillTelemetry(): Promise<SkillTelemetryRow[]> { this.postgresOnly(\"skill telemetry\"); }\n\n // -------------------------------------------------------------------------\n // File-path rename (session cleanup: moving Notes/*.md into YYYY/MM/)\n // -------------------------------------------------------------------------\n\n async countFilesWithPaths(paths: string[]): Promise<number> {\n if (paths.length === 0) return 0;\n const placeholders = paths.map(() => \"?\").join(\", \");\n const row = this.db\n .prepare(`SELECT COUNT(*) AS n FROM memory_files WHERE path IN (${placeholders})`)\n .get(...paths) as { n: number };\n return row.n;\n }\n\n async renameFilePaths(moves: Array<{ oldPath: string; newPath: string }>): Promise<number> {\n if (moves.length === 0) return 0;\n const updateFiles = this.db.prepare(\"UPDATE memory_files SET path = ? WHERE path = ?\");\n const updateChunks = this.db.prepare(\"UPDATE memory_chunks SET path = ? WHERE path = ?\");\n let filesUpdated = 0;\n this.db.transaction(() => {\n for (const { oldPath, newPath } of moves) {\n const r = updateFiles.run(newPath, oldPath);\n filesUpdated += r.changes;\n updateChunks.run(newPath, oldPath);\n }\n })();\n return filesUpdated;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sqlite-CfKfuggn.mjs","names":[],"sources":["../src/storage/sqlite/indexer.ts","../src/storage/sqlite/tunnels.ts","../src/storage/sqlite/sources.ts","../src/storage/sqlite/search.ts","../src/storage/sqlite/kg-entity.ts","../src/storage/sqlite.ts"],"sourcesContent":["/**\n * Synchronous (SQLite) indexer — moved from memory/indexer/sync.ts so the\n * only file that opens prepared statements against the federation database\n * lives under src/storage/ (design doc docs/design/postgres-only.md, unit 7).\n *\n * Scans project memory/ and Notes/ directories, chunks markdown files, and\n * inserts the resulting chunks into the federation memory store for BM25\n * search. Uses raw better-sqlite3 Database directly for maximum SQLite\n * performance (synchronous transactions, no serialisation overhead) — the\n * reason SQLiteBackend keeps this path instead of routing through the\n * generic (StorageBackend) indexer that Postgres uses.\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport { chunkMarkdown } from \"../../memory/chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"../../memory/indexer/helpers.js\";\nimport type { IndexResult } from \"../../memory/indexer/types.js\";\n\n// ---------------------------------------------------------------------------\n// Single-file indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file into the federation database.\n *\n * @returns true if the file was re-indexed (changed or new), false if skipped.\n */\nfunction indexFile(\n db: Database,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): boolean {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n // File unreadable or missing — skip silently\n return false;\n }\n\n const hash = sha256File(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n const existing = db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, relativePath) as { hash: string } | undefined;\n\n if (existing?.hash === hash) {\n return false;\n }\n\n const oldChunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, relativePath) as Array<{ id: string }>;\n\n const deleteFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunk = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const row of oldChunkIds) {\n deleteFts.run(row.id);\n }\n deleteChunk.run(projectId, relativePath);\n })();\n\n const chunks = chunkMarkdown(content);\n\n const insertChunk = db.prepare(`\n INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const insertFts = db.prepare(`\n INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n const upsertFile = db.prepare(`\n INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size\n `);\n\n const updatedAt = Date.now();\n\n db.transaction(() => {\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(projectId, relativePath, i, chunk.startLine, chunk.endLine);\n insertChunk.run(\n id, projectId, source, tier, relativePath,\n chunk.startLine, chunk.endLine, chunk.hash, chunk.text, updatedAt,\n );\n insertFts.run(\n chunk.text, id, projectId, relativePath, source, tier,\n chunk.startLine, chunk.endLine,\n );\n }\n upsertFile.run(projectId, relativePath, source, tier, hash, mtime, size);\n })();\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing\n// ---------------------------------------------------------------------------\n\n/**\n * Index all memory, Notes, and content files for a single registered project.\n */\nasync function indexProject(\n db: Database,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files with the standard filename format:\n // \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n {\n const titleInsertChunk = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n {\n const updatedAt = Date.now();\n const titleInsertChunk2 = db.prepare(`\n INSERT OR IGNORE INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n const titleInsertFts2 = db.prepare(`\n INSERT OR IGNORE INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n `);\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n db.transaction(() => {\n titleInsertChunk2.run(id, projectId, \"notes\", \"session\", syntheticPath, 0, 0, hash, text, updatedAt);\n titleInsertFts2.run(text, id, projectId, syntheticPath, \"notes\", \"session\", 0, 0);\n })();\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({\n absPath: claudeMemoryMd,\n rootBase: claudeProjectDir,\n source: \"memory\",\n tier: \"evergreen\",\n });\n }\n\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = indexFile(db, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const count = db\n .prepare(\"SELECT COUNT(*) as n FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .get(projectId, relPath) as { n: number };\n\n result.filesProcessed++;\n result.chunksCreated += count.n;\n } else {\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths: remove DB entries for files that no longer exist on disk.\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n\n const stalePaths: string[] = [];\n for (const row of dbChunkPaths) {\n const basePath = row.path.endsWith(\"::title\")\n ? row.path.slice(0, -\"::title\".length)\n : row.path;\n if (!livePaths.has(basePath)) {\n stalePaths.push(row.path);\n }\n }\n\n if (stalePaths.length > 0) {\n const deleteChunksFts = db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\",\n );\n const deleteFile = db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\",\n );\n\n db.transaction(() => {\n for (const stalePath of stalePaths) {\n const chunkIds = db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, stalePath) as Array<{ id: string }>;\n for (const { id } of chunkIds) {\n deleteChunksFts.run(id);\n }\n deleteChunks.run(projectId, stalePath);\n deleteFile.run(projectId, stalePath);\n }\n })();\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing (all registered projects)\n// ---------------------------------------------------------------------------\n\n/**\n * Index all active projects registered in the registry DB.\n *\n * Async: yields to the event loop between each project so that the daemon's\n * Unix socket server can process IPC requests (e.g. status) while indexing.\n */\nexport async function indexAllSqlite(\n db: Database,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = {\n filesProcessed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProject(db, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n","/**\n * SQLite implementation of cross-project concept \"tunnel\" detection — moved\n * from memory/tunnels.ts so the only file that runs raw FTS5/vocab SQL\n * against the federation database lives under src/storage/.\n *\n * Algorithm: pull candidate terms from the FTS5 vocabulary shadow table (or\n * sample chunk text when the vocab view is unavailable), then for each\n * candidate count distinct projects and total occurrences.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { STOP_WORDS } from \"../../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../registry-interface.js\";\nimport type { Tunnel, FindTunnelsOptions, FindTunnelsResult } from \"../../memory/tunnels.js\";\n\ninterface ProjectSlugMap {\n [id: number]: string;\n}\n\nasync function findTunnelsForSlugMap(\n db: Database,\n slugMap: ProjectSlugMap,\n opts: Required<FindTunnelsOptions>,\n): Promise<FindTunnelsResult> {\n const projectIds = Object.keys(slugMap).map(Number);\n if (projectIds.length < 2) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 1 — collect candidate terms via FTS5 vocabulary shadow table.\n let candidateTerms: string[] = [];\n\n try {\n const vocabRows = db\n .prepare(\n `SELECT term, SUM(doc) AS doc_count, SUM(cnt) AS total_cnt\n FROM memory_fts_v\n GROUP BY term\n HAVING SUM(cnt) >= ?\n ORDER BY SUM(doc) DESC\n LIMIT 500`\n )\n .all(opts.min_occurrences) as Array<{ term: string; doc_count: number; total_cnt: number }>;\n\n candidateTerms = vocabRows\n .map((r) => r.term)\n .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));\n } catch {\n // Vocab table not available — fall back to sampling common words from chunks.\n const sampleRows = db\n .prepare(\n `SELECT LOWER(text) AS text FROM memory_chunks\n WHERE LENGTH(text) > 20\n ORDER BY RANDOM()\n LIMIT 2000`\n )\n .all() as Array<{ text: string }>;\n\n const freq = new Map<string, number>();\n for (const { text } of sampleRows) {\n const tokens = text\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t: string) => t.length >= 3 && !STOP_WORDS.has(t));\n for (const t of tokens) {\n freq.set(t, (freq.get(t) ?? 0) + 1);\n }\n }\n candidateTerms = [...freq.entries()]\n .filter(([, n]) => n >= opts.min_occurrences)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 200)\n .map(([t]) => t);\n }\n\n if (candidateTerms.length === 0) {\n return { tunnels: [], projects_analyzed: projectIds.length, total_concepts_evaluated: 0 };\n }\n\n // Step 2 — for each candidate, count distinct projects and occurrences.\n const tunnels: Tunnel[] = [];\n\n for (const term of candidateTerms) {\n try {\n const rows = db\n .prepare(\n `SELECT c.project_id, COUNT(*) AS cnt,\n MIN(c.updated_at) AS first_seen,\n MAX(c.updated_at) AS last_seen\n FROM memory_fts f\n JOIN memory_chunks c ON c.id = f.id\n WHERE memory_fts MATCH ?\n AND c.project_id IN (${projectIds.map(() => \"?\").join(\", \")})\n GROUP BY c.project_id`\n )\n .all(`\"${term.replace(/\"/g, '\"\"')}\"`, ...projectIds) as Array<{\n project_id: number;\n cnt: number;\n first_seen: number;\n last_seen: number;\n }>;\n\n if (rows.length < opts.min_projects) continue;\n\n const totalOccurrences = rows.reduce((s, r) => s + Number(r.cnt), 0);\n if (totalOccurrences < opts.min_occurrences) continue;\n\n const projects = rows\n .map((r) => slugMap[r.project_id] ?? String(r.project_id))\n .filter(Boolean);\n const firstSeen = Math.min(...rows.map((r) => r.first_seen));\n const lastSeen = Math.max(...rows.map((r) => r.last_seen));\n\n tunnels.push({\n concept: term,\n projects,\n occurrences: totalOccurrences,\n first_seen: firstSeen,\n last_seen: lastSeen,\n });\n } catch {\n // Skip problematic terms\n continue;\n }\n }\n\n tunnels.sort((a, b) => {\n const byProjects = b.projects.length - a.projects.length;\n if (byProjects !== 0) return byProjects;\n return b.occurrences - a.occurrences;\n });\n\n return {\n tunnels: tunnels.slice(0, opts.limit),\n projects_analyzed: projectIds.length,\n total_concepts_evaluated: candidateTerms.length,\n };\n}\n\n/** Find cross-project concept tunnels against a SQLite federation database. */\nexport async function findTunnelsSqlite(\n db: Database,\n registry: RegistryBackend,\n options?: FindTunnelsOptions,\n): Promise<FindTunnelsResult> {\n const opts: Required<FindTunnelsOptions> = {\n min_projects: options?.min_projects ?? 2,\n min_occurrences: options?.min_occurrences ?? 3,\n limit: options?.limit ?? 20,\n };\n\n const projectRows = await registry.listProjects({ excludeArchived: true });\n const slugMap: ProjectSlugMap = {};\n for (const { id, slug } of projectRows) {\n slugMap[id] = slug;\n }\n\n return findTunnelsForSlugMap(db, slugMap, opts);\n}\n","/**\n * SQLite implementation of the `pai memory sources` aggregate report — moved\n * from cli/commands/memory/sources.ts so the only file running ad-hoc\n * aggregate SQL against the federation database lives under src/storage/.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { MemorySourcesReport } from \"../interface.js\";\n\nexport function getMemorySourcesReportSqlite(db: Database): MemorySourcesReport {\n const composition = db\n .prepare(\n `SELECT source, tier, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY source, tier ORDER BY COUNT(*) DESC`\n )\n .all() as Array<{ source: string; tier: string; chunks: number; embedded: number }>;\n\n const paths = db\n .prepare(`SELECT path, COUNT(*) AS chunks FROM memory_chunks GROUP BY path`)\n .all() as Array<{ path: string; chunks: number }>;\n\n const churn = db\n .prepare(\n `SELECT date(updated_at/1000,'unixepoch') AS day, COUNT(*) AS chunks,\n SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS embedded\n FROM memory_chunks GROUP BY day ORDER BY day DESC LIMIT 10`\n )\n .all() as Array<{ day: string; chunks: number; embedded: number }>;\n\n return { composition, paths, churn };\n}\n","/**\n * SQLite search implementation — moved from memory/search.ts so the only\n * file running raw FTS5/vector queries against the federation database\n * lives under src/storage/. Query building, slug population, and recency\n * boosting are pure logic with no SQL and stay in memory/search.ts.\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via the indexer's embed pass.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../../memory/embeddings.js\";\nimport {\n buildFtsQuery,\n isQuerySyntaxError,\n type SearchResult,\n type SearchOptions,\n} from \"../../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Keyword (BM25) search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n const sql = `\n SELECT\n c.id,\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n c.updated_at,\n c.last_accessed_at,\n c.relevance_score,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch (e) {\n // FTS5 MATCH throws on a malformed query, and for THAT an empty result is the\n // honest answer — nothing matches a query that cannot be parsed.\n //\n // Everything else is a failure of the store: a missing table, a corrupt\n // index, a locked database. Those used to return [] as well, which made an\n // unusable index byte-identical to a genuine miss. The Postgres path had the\n // same defect and it cost a real wrong answer on 2026-08-04 — the backend was\n // down for two hours, every search reported \"No results found\", and a sibling\n // session told the owner a DMARC note did not exist. See\n // storage/postgres/search.ts.\n if (!isQuerySyntaxError(e)) {\n throw new Error(\n `Memory keyword search failed — the index is unusable, so this is NOT an ` +\n `empty result set. Cause: ${e instanceof Error ? e.message : String(e)}`\n );\n }\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => {\n const baseScore = -row.bm25_score;\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n })\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered.\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding, updated_at, last_accessed_at, relevance_score\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n updated_at: number;\n last_accessed_at: number | null;\n relevance_score: number | null;\n }>;\n\n if (rows.length === 0) return [];\n\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const baseScore = cosineSimilarity(queryEmbedding, vec);\n const relevanceScore = row.relevance_score ?? 0.5;\n const score = baseScore * (0.5 + relevanceScore);\n return {\n chunkId: row.id,\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n updatedAt: row.updated_at,\n lastAccessedAt: row.last_accessed_at ?? undefined,\n };\n });\n\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n const keywordResults = searchMemory(db, query, { ...opts, maxResults: 50 });\n const semanticResults = searchMemorySemantic(db, queryEmbedding, { ...opts, maxResults: 50 });\n\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 = kw * kwScore + sw * 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// ---------------------------------------------------------------------------\n// Access timestamp tracking (QW2)\n// ---------------------------------------------------------------------------\n\n/**\n * Update last_accessed_at for a set of chunk IDs to the current timestamp.\n *\n * Best-effort: errors are silently ignored so search is never blocked.\n */\nexport function touchChunksLastAccessed(db: Database, chunkIds: string[]): void {\n if (chunkIds.length === 0) return;\n try {\n const now = Date.now();\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n db.prepare(\n `UPDATE memory_chunks SET last_accessed_at = ? WHERE id IN (${placeholders})`\n ).run(now, ...chunkIds);\n } catch {\n // non-critical — do not block search results\n }\n}\n","/**\n * SQLite kg_entities CRUD — moved from memory/kg-entity.ts so the only file\n * running raw SQL against kg_entities lives under src/storage/. The pure\n * content-addressing helpers (entityContentId/edgeContentId) and the\n * KgEntity/KgEntityUpsertParams types they share with PostgresBackend stay\n * in memory/kg-entity.ts.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { entityContentId, type KgEntity, type KgEntityUpsertParams } from \"../../memory/kg-entity.js\";\n\n/**\n * Upsert a KG entity in the federation SQLite database.\n *\n * If the entity already exists for this tenant:\n * - Updates last_seen to now\n * - Increments mention_count\n * - Updates description if provided (overwrites older description)\n *\n * Returns the entity_id for use as a foreign key in kg_triples.\n */\nexport function upsertKgEntitySqlite(\n db: Database,\n params: KgEntityUpsertParams\n): string {\n const tenantId = params.tenantId ?? \"default\";\n const entityId = entityContentId(params.name, tenantId);\n const now = Date.now();\n\n db.prepare(`\n INSERT INTO kg_entities\n (entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)\n VALUES\n (?, ?, ?, ?, ?, ?, ?, 1, 0.5)\n ON CONFLICT(entity_id) DO UPDATE SET\n last_seen = excluded.last_seen,\n mention_count = mention_count + 1,\n description = COALESCE(excluded.description, description),\n type = CASE WHEN excluded.type != 'unknown' THEN excluded.type ELSE type END\n `).run(\n entityId,\n tenantId,\n params.name,\n params.type ?? \"unknown\",\n params.description ?? null,\n now,\n now\n );\n\n return entityId;\n}\n\n/**\n * Look up a KG entity by name within a tenant.\n * Returns null if the entity does not exist.\n */\nexport function findKgEntitySqlite(\n db: Database,\n name: string,\n tenantId = \"default\"\n): KgEntity | null {\n const entityId = entityContentId(name, tenantId);\n const row = db.prepare(\n \"SELECT * FROM kg_entities WHERE entity_id = ? AND tenant_id = ?\"\n ).get(entityId, tenantId) as KgEntity | undefined;\n return row ?? null;\n}\n\n/**\n * List KG entities for a tenant, optionally filtered by type.\n */\nexport function listKgEntitiesSqlite(\n db: Database,\n tenantId = \"default\",\n type?: string,\n limit = 100\n): KgEntity[] {\n if (type) {\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? AND type = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, type, limit) as KgEntity[];\n }\n return db.prepare(\n \"SELECT * FROM kg_entities WHERE tenant_id = ? ORDER BY mention_count DESC LIMIT ?\"\n ).all(tenantId, limit) as KgEntity[];\n}\n\n/**\n * Apply an EMA (Exponential Moving Average) feedback update to an entity's weight.\n *\n * EMA formula: new_weight = old_weight + alpha * (target - old_weight)\n */\nexport function updateEntityFeedbackWeightSqlite(\n db: Database,\n entityId: string,\n normalizedRating: number,\n alpha = 0.1\n): void {\n const row = db.prepare(\n \"SELECT feedback_weight FROM kg_entities WHERE entity_id = ?\"\n ).get(entityId) as { feedback_weight: number } | undefined;\n\n if (!row) return;\n\n const newWeight = row.feedback_weight + alpha * (normalizedRating - row.feedback_weight);\n db.prepare(\n \"UPDATE kg_entities SET feedback_weight = ? WHERE entity_id = ?\"\n ).run(newWeight, entityId);\n}\n","/**\n * SQLiteBackend — wraps the existing better-sqlite3 federation.db\n * behind the StorageBackend interface.\n *\n * This is a thin adapter. The heavy lifting is all in the existing\n * memory/indexer.ts and memory/search.ts code; we just provide a\n * backend-agnostic surface so the daemon and tools can call either\n * SQLite or Postgres transparently.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type {\n StorageBackend, ChunkRow, FileRow, FederationStats,\n MemorySourcesReport, FindTunnelsOptions, FindTunnelsResult,\n KgStats, ObservationRow, ObservationWithCwd, StoreObservationInput, QueryObservationsOptions,\n ObservationStats, SessionSummaryRow, StoreSessionSummaryInput,\n SkillTelemetryRow, RecordSkillInvocationInput, QuerySkillTelemetryOptions,\n} from \"./interface.js\";\nimport type { SearchResult, SearchOptions } from \"../memory/search.js\";\nimport type { KgEntity, KgEntityUpsertParams } from \"../memory/kg-entity.js\";\nimport type { KgAddParams, KgQueryParams, KgTriple, KgContradiction } from \"../memory/kg.js\";\nimport type { IndexResult } from \"../memory/indexer/types.js\";\nimport type { RegistryBackend } from \"./registry-interface.js\";\nimport { indexAllSqlite } from \"./sqlite/indexer.js\";\nimport { findTunnelsSqlite } from \"./sqlite/tunnels.js\";\nimport { getMemorySourcesReportSqlite } from \"./sqlite/sources.js\";\nimport { searchMemory, searchMemorySemantic, touchChunksLastAccessed } from \"./sqlite/search.js\";\nimport {\n upsertKgEntitySqlite,\n findKgEntitySqlite,\n listKgEntitiesSqlite,\n updateEntityFeedbackWeightSqlite,\n} from \"./sqlite/kg-entity.js\";\n\nexport class SQLiteBackend implements StorageBackend {\n readonly backendType = \"sqlite\" as const;\n readonly supportsPostgresFeatures = false;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /**\n * Expose the raw better-sqlite3 Database handle. Storage-internal only —\n * used by db-admin.ts for `pai db query|tables|schema` (the one place\n * outside this class allowed to reach for it). Every other operation must\n * be a StorageBackend method; nothing outside src/storage/ may call this.\n */\n getRawDb(): Database {\n return this.db;\n }\n\n // -------------------------------------------------------------------------\n // Lifecycle\n // -------------------------------------------------------------------------\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async getStats(): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files\").get() as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks\").get() as { n: number }\n ).n;\n return { files, chunks };\n }\n\n async getProjectStats(projectId: number): Promise<FederationStats> {\n const files = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_files WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n const chunks = (\n this.db.prepare(\"SELECT COUNT(*) AS n FROM memory_chunks WHERE project_id = ?\").get(projectId) as { n: number }\n ).n;\n return { files, chunks };\n }\n\n // -------------------------------------------------------------------------\n // File tracking\n // -------------------------------------------------------------------------\n\n async getFileHash(projectId: number, path: string): Promise<string | undefined> {\n const row = this.db\n .prepare(\"SELECT hash FROM memory_files WHERE project_id = ? AND path = ?\")\n .get(projectId, path) as { hash: string } | undefined;\n return row?.hash;\n }\n\n async upsertFile(file: FileRow): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO memory_files (project_id, path, source, tier, hash, mtime, size)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, path) DO UPDATE SET\n source = excluded.source,\n tier = excluded.tier,\n hash = excluded.hash,\n mtime = excluded.mtime,\n size = excluded.size`\n )\n .run(file.projectId, file.path, file.source, file.tier, file.hash, file.mtime, file.size);\n }\n\n // -------------------------------------------------------------------------\n // Chunk management\n // -------------------------------------------------------------------------\n\n async getChunkIds(projectId: number, path: string): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n }\n\n async deleteChunksForFile(projectId: number, path: string): Promise<void> {\n const ids = await this.getChunkIds(projectId, path);\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const id of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n })();\n }\n\n async insertChunks(chunks: ChunkRow[]): Promise<void> {\n if (chunks.length === 0) return;\n\n const insertChunk = this.db.prepare(\n `INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n );\n const insertFts = this.db.prepare(\n `INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n );\n\n this.db.transaction(() => {\n for (const c of chunks) {\n insertChunk.run(\n c.id,\n c.projectId,\n c.source,\n c.tier,\n c.path,\n c.startLine,\n c.endLine,\n c.hash,\n c.text,\n c.updatedAt\n );\n insertFts.run(\n c.text,\n c.id,\n c.projectId,\n c.path,\n c.source,\n c.tier,\n c.startLine,\n c.endLine\n );\n }\n })();\n }\n\n async getDistinctChunkPaths(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\"SELECT DISTINCT path FROM memory_chunks WHERE project_id = ?\")\n .all(projectId) as Array<{ path: string }>;\n return rows.map((r) => r.path);\n }\n\n async deletePaths(projectId: number, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const deleteFts = this.db.prepare(\"DELETE FROM memory_fts WHERE id = ?\");\n const deleteChunks = this.db.prepare(\n \"DELETE FROM memory_chunks WHERE project_id = ? AND path = ?\"\n );\n const deleteFile = this.db.prepare(\n \"DELETE FROM memory_files WHERE project_id = ? AND path = ?\"\n );\n this.db.transaction(() => {\n for (const path of paths) {\n const ids = this.db\n .prepare(\"SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?\")\n .all(projectId, path) as Array<{ id: string }>;\n for (const { id } of ids) {\n deleteFts.run(id);\n }\n deleteChunks.run(projectId, path);\n deleteFile.run(projectId, path);\n }\n })();\n }\n\n async getUnembeddedChunkIds(\n projectId?: number,\n limit?: number,\n after?: { projectId: number; id: string } | null,\n ): Promise<Array<{ id: string; text: string; project_id: number; path: string }>> {\n const conditions = [\"embedding IS NULL\"];\n const params: (string | number)[] = [];\n\n if (projectId !== undefined) {\n conditions.push(\"project_id = ?\");\n params.push(projectId);\n }\n\n // Once paginating, the priority ORDER BY below no longer applies — pages\n // must be ordered by the same (project_id, id) the cursor seeks on.\n const paginating = after !== undefined;\n if (after) {\n conditions.push(\"(project_id > ? OR (project_id = ? AND id > ?))\");\n params.push(after.projectId, after.projectId, after.id);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n // Prioritize real knowledge notes over PAI session/job-search noise.\n // CASE expression assigns lower priority numbers to knowledge paths.\n const orderBy = paginating\n ? \"project_id, id\"\n : `CASE\n WHEN path LIKE '🧠 Ideaverse/%' THEN 0\n WHEN path LIKE 'Z - Zettelkasten/%' THEN 0\n WHEN path LIKE '💼 Business/%' THEN 0\n WHEN path LIKE '📆 Meetings/%' THEN 1\n WHEN path LIKE '💡 Insights/%' THEN 1\n WHEN path LIKE '👨💻 People/%' THEN 1\n WHEN path LIKE 'University/%' THEN 1\n WHEN path LIKE 'Copilot/%' THEN 1\n WHEN path LIKE '🗓️ Daily Notes/%' THEN 2\n WHEN path LIKE 'PAI/%' THEN 3\n WHEN path LIKE '09-job-search/%' THEN 4\n WHEN path LIKE 'seriousletter/%' THEN 4\n WHEN path LIKE 'Attachments/%' THEN 5\n ELSE 2\n END, id`;\n const rows = this.db\n .prepare(`SELECT id, text, project_id, path FROM memory_chunks ${where}\n ORDER BY ${orderBy}${limit !== undefined ? \" LIMIT ?\" : \"\"}`)\n .all(...params, ...(limit !== undefined ? [limit] : [])) as Array<{ id: string; text: string; project_id: number; path: string }>;\n return rows;\n }\n\n async updateEmbedding(chunkId: string, embedding: Buffer): Promise<void> {\n this.db\n .prepare(\"UPDATE memory_chunks SET embedding = ? WHERE id = ?\")\n .run(embedding, chunkId);\n }\n\n // -------------------------------------------------------------------------\n // Search\n // -------------------------------------------------------------------------\n\n async searchKeyword(query: string, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemory(this.db, query, opts);\n }\n\n async searchSemantic(queryEmbedding: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n return searchMemorySemantic(this.db, queryEmbedding, opts);\n }\n\n // -------------------------------------------------------------------------\n // Vault operations — not supported on SQLite backend (use Postgres)\n // -------------------------------------------------------------------------\n\n private vaultNotSupported(): never {\n throw new Error(\"Vault operations require the Postgres backend\");\n }\n\n async upsertVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultFile(): Promise<void> { this.vaultNotSupported(); }\n async getVaultFile(): Promise<null> { this.vaultNotSupported(); }\n async getVaultFileByInode(): Promise<null> { this.vaultNotSupported(); }\n async getAllVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async getRecentVaultFiles(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFiles(): Promise<number> { this.vaultNotSupported(); }\n async upsertVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async deleteVaultAliases(): Promise<void> { this.vaultNotSupported(); }\n async replaceLinksForSources(): Promise<void> { this.vaultNotSupported(); }\n async getLinksFromSource(): Promise<never[]> { this.vaultNotSupported(); }\n async getLinksToTarget(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkGraph(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertVaultHealth(): Promise<void> { this.vaultNotSupported(); }\n async getVaultHealth(): Promise<null> { this.vaultNotSupported(); }\n async getOrphans(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinks(): Promise<never[]> { this.vaultNotSupported(); }\n async upsertNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async replaceNameIndex(): Promise<void> { this.vaultNotSupported(); }\n async resolveVaultName(): Promise<never[]> { this.vaultNotSupported(); }\n async searchVaultNameIndex(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilesByPathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinksFromPaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksWithEmbeddings(): Promise<never[]> { this.vaultNotSupported(); }\n async getChunksForPath(): Promise<never[]> { this.vaultNotSupported(); }\n async searchChunksByText(): Promise<never[]> { this.vaultNotSupported(); }\n async countVaultFilesWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultFilesAfter(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksWithPrefix(): Promise<number> { this.vaultNotSupported(); }\n async countVaultLinksAfter(): Promise<number> { this.vaultNotSupported(); }\n async getDeadLinksWithLineNumbers(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getDeadLinksAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getOrphansAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivity(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getLowConnectivityAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getAllVaultFilePaths(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultFilePathsAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdges(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesWithPrefix(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultLinkEdgesAfter(): Promise<never[]> { this.vaultNotSupported(); }\n async getVaultAlias(): Promise<null> { this.vaultNotSupported(); }\n\n // -------------------------------------------------------------------------\n // Knowledge-graph entities (kg_entities) — wraps storage/sqlite/kg-entity.ts\n // -------------------------------------------------------------------------\n\n async upsertKgEntity(params: KgEntityUpsertParams): Promise<string> {\n return upsertKgEntitySqlite(this.db, params);\n }\n\n async findKgEntity(name: string, tenantId = \"default\"): Promise<KgEntity | null> {\n return findKgEntitySqlite(this.db, name, tenantId);\n }\n\n async listKgEntities(tenantId = \"default\", type?: string, limit = 100): Promise<KgEntity[]> {\n return listKgEntitiesSqlite(this.db, tenantId, type, limit);\n }\n\n async updateEntityFeedbackWeight(entityId: string, normalizedRating: number, alpha = 0.1): Promise<void> {\n return updateEntityFeedbackWeightSqlite(this.db, entityId, normalizedRating, alpha);\n }\n\n // -------------------------------------------------------------------------\n // Chunk feedback / access tracking\n // -------------------------------------------------------------------------\n\n async getChunksForFeedback(chunkIds: string[]): Promise<Array<{ id: string; text: string; relevanceScore: number | null }>> {\n if (chunkIds.length === 0) return [];\n const placeholders = chunkIds.map(() => \"?\").join(\", \");\n const rows = this.db\n .prepare(`SELECT id, text, relevance_score FROM memory_chunks WHERE id IN (${placeholders})`)\n .all(...chunkIds) as Array<{ id: string; text: string; relevance_score: number | null }>;\n return rows.map((r) => ({ id: r.id, text: r.text, relevanceScore: r.relevance_score }));\n }\n\n async updateChunkRelevanceScore(chunkId: string, score: number): Promise<void> {\n this.db.prepare(\"UPDATE memory_chunks SET relevance_score = ? WHERE id = ?\").run(score, chunkId);\n }\n\n async touchChunksLastAccessed(chunkIds: string[]): Promise<void> {\n touchChunksLastAccessed(this.db, chunkIds);\n }\n\n // -------------------------------------------------------------------------\n // Global indexing\n // -------------------------------------------------------------------------\n\n async indexAll(registry: RegistryBackend): Promise<{ projects: number; result: IndexResult }> {\n return indexAllSqlite(this.db, registry);\n }\n\n // -------------------------------------------------------------------------\n // Cross-project concept tunnels\n // -------------------------------------------------------------------------\n\n async findTunnels(registry: RegistryBackend, options?: FindTunnelsOptions): Promise<FindTunnelsResult> {\n return findTunnelsSqlite(this.db, registry, options);\n }\n\n // -------------------------------------------------------------------------\n // Memory sources report\n // -------------------------------------------------------------------------\n\n async getMemorySourcesReport(): Promise<MemorySourcesReport> {\n return getMemorySourcesReportSqlite(this.db);\n }\n\n // -------------------------------------------------------------------------\n // Temporal knowledge graph / observations / skill telemetry — not\n // supported on SQLite (kg_triples, pai_observations, pai_session_summaries,\n // pai_skill_telemetry only exist in the Postgres schema).\n // -------------------------------------------------------------------------\n\n private postgresOnly(feature: string): never {\n throw new Error(`${feature} is not supported on the sqlite backend — set \"storageBackend\": \"postgres\" in the daemon config`);\n }\n\n async addKgTriple(): Promise<KgTriple> { this.postgresOnly(\"kg_triples\"); }\n async queryKgTriples(): Promise<KgTriple[]> { this.postgresOnly(\"kg_triples\"); }\n async invalidateKgTriple(): Promise<void> { this.postgresOnly(\"kg_triples\"); }\n async getKgContradictions(): Promise<KgContradiction[]> { this.postgresOnly(\"kg_triples\"); }\n async getKgStats(): Promise<KgStats> { this.postgresOnly(\"kg_triples\"); }\n\n async storeObservation(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async storeObservationWithProject(): Promise<number | null> { this.postgresOnly(\"observations\"); }\n async queryObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async queryRecentObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async querySessionObservations(): Promise<ObservationRow[]> { this.postgresOnly(\"observations\"); }\n async getObservationStats(): Promise<ObservationStats> { this.postgresOnly(\"observations\"); }\n async getObservationTypesForPaths(): Promise<Map<string, Record<string, number>>> { return new Map(); }\n async storeSessionSummary(): Promise<void> { this.postgresOnly(\"session summaries\"); }\n async queryRecentSummaries(): Promise<SessionSummaryRow[]> { this.postgresOnly(\"session summaries\"); }\n async recordSkillInvocation(): Promise<void> { this.postgresOnly(\"skill telemetry\"); }\n async querySkillTelemetry(): Promise<SkillTelemetryRow[]> { this.postgresOnly(\"skill telemetry\"); }\n\n // -------------------------------------------------------------------------\n // File-path rename (session cleanup: moving Notes/*.md into YYYY/MM/)\n // -------------------------------------------------------------------------\n\n async countFilesWithPaths(paths: string[]): Promise<number> {\n if (paths.length === 0) return 0;\n const placeholders = paths.map(() => \"?\").join(\", \");\n const row = this.db\n .prepare(`SELECT COUNT(*) AS n FROM memory_files WHERE path IN (${placeholders})`)\n .get(...paths) as { n: number };\n return row.n;\n }\n\n async renameFilePaths(moves: Array<{ oldPath: string; newPath: string }>): Promise<number> {\n if (moves.length === 0) return 0;\n const updateFiles = this.db.prepare(\"UPDATE memory_files SET path = ? WHERE path = ?\");\n const updateChunks = this.db.prepare(\"UPDATE memory_chunks SET path = ? WHERE path = ?\");\n let filesUpdated = 0;\n this.db.transaction(() => {\n for (const { oldPath, newPath } of moves) {\n const r = updateFiles.run(newPath, oldPath);\n filesUpdated += r.changes;\n updateChunks.run(newPath, oldPath);\n }\n })();\n return filesUpdated;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "./factory-
|
|
2
|
-
import "./wakeup-
|
|
1
|
+
import "./factory-BozZa81q.mjs";
|
|
2
|
+
import "./wakeup-DO1O4udw.mjs";
|
|
3
3
|
import "./embeddings-DfrYNIng.mjs";
|
|
4
|
-
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-
|
|
4
|
+
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-DFdPbX-X.mjs";
|
|
5
5
|
|
|
6
6
|
export { toolKgAdd, toolKgContradictions, toolKgInvalidate, toolKgQuery, toolMemoryTunnels, toolZettelCommunities, toolZettelConverse, toolZettelExplore, toolZettelGodNotes, toolZettelHealth, toolZettelSuggest, toolZettelSurprise, toolZettelThemes };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as populateSlugs } from "./search-BaTbyWjK.mjs";
|
|
2
|
-
import { a as formatDetectionJson, r as detectProject, t as buildWakeupContext } from "./wakeup-
|
|
2
|
+
import { a as formatDetectionJson, r as detectProject, t as buildWakeupContext } from "./wakeup-DO1O4udw.mjs";
|
|
3
3
|
import { n as cosineSimilarity } from "./embeddings-DfrYNIng.mjs";
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
@@ -1470,4 +1470,4 @@ async function toolMemoryKgSearch(federation, params) {
|
|
|
1470
1470
|
|
|
1471
1471
|
//#endregion
|
|
1472
1472
|
export { formatProject as A, toolProjectInfo as C, toolMemoryGet as D, combineHybridResults as E, toolMemorySearch as O, toolProjectHealth as S, toolProjectTodo as T, toolZettelThemes as _, toolMemoryWakeup as a, toolSessionRoute as b, toolKgInvalidate as c, toolZettelConverse as d, toolZettelExplore as f, toolZettelSurprise as g, toolZettelSuggest as h, toolMemoryTaxonomy as i, lookupProjectId as j, detectProjectFromPath as k, toolKgQuery as l, toolZettelHealth as m, toolMemoryFeedback as n, toolKgAdd as o, toolZettelGodNotes as p, toolMemoryTunnels as r, toolKgContradictions as s, toolMemoryKgSearch as t, toolZettelCommunities as u, toolRegistrySearch as v, toolProjectList as w, toolProjectDetect as x, toolSessionList as y };
|
|
1473
|
-
//# sourceMappingURL=tools-
|
|
1473
|
+
//# sourceMappingURL=tools-DFdPbX-X.mjs.map
|