@tekmidian/pai 0.8.4 → 0.8.5
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/cli/index.mjs +11 -11
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon-nXyhvdzz.mjs → daemon-BaYX-w_d.mjs} +10 -6
- package/dist/daemon-BaYX-w_d.mjs.map +1 -0
- package/dist/{factory-Ygqe_bVZ.mjs → factory-BzWfxsvK.mjs} +2 -2
- package/dist/{factory-Ygqe_bVZ.mjs.map → factory-BzWfxsvK.mjs.map} +1 -1
- package/dist/{postgres-CKf-EDtS.mjs → postgres-DbUXNuy_.mjs} +24 -10
- package/dist/postgres-DbUXNuy_.mjs.map +1 -0
- package/dist/query-feedback-Dv43XKHM.mjs +76 -0
- package/dist/query-feedback-Dv43XKHM.mjs.map +1 -0
- package/dist/{tools-DcaJlYDN.mjs → tools-BXSwlzeH.mjs} +76 -7
- package/dist/tools-BXSwlzeH.mjs.map +1 -0
- package/dist/{vault-indexer-Bi2cRmn7.mjs → vault-indexer-B-aJpRZC.mjs} +3 -2
- package/dist/{vault-indexer-Bi2cRmn7.mjs.map → vault-indexer-B-aJpRZC.mjs.map} +1 -1
- package/dist/{zettelkasten-cdajbnPr.mjs → zettelkasten-DhBKZQHF.mjs} +358 -3
- package/dist/zettelkasten-DhBKZQHF.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/daemon-nXyhvdzz.mjs.map +0 -1
- package/dist/postgres-CKf-EDtS.mjs.map +0 -1
- package/dist/tools-DcaJlYDN.mjs.map +0 -1
- package/dist/zettelkasten-cdajbnPr.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-BaYX-w_d.mjs","names":["resolve","homedir","existsSync","readFileSync","join","join","basename","existsSync","readdirSync","join","existsSync","readdirSync","basename","readFileSync","existsSync","join","readFileSync","contentToText","contentToText","getQueueStats"],"sources":["../src/notifications/config.ts","../src/daemon/daemon/scheduler.ts","../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts","../src/observations/store.ts","../src/daemon/daemon/dispatcher.ts","../src/daemon/work-queue.ts","../src/hooks/ts/lib/pai-paths.ts","../src/hooks/ts/lib/project-utils/paths.ts","../src/hooks/ts/lib/project-utils/session-notes.ts","../src/hooks/ts/lib/project-utils/todo.ts","../src/daemon/templates/session-summary-prompt.ts","../src/daemon/session-summary-worker.ts","../src/daemon/topic-detect-worker.ts","../src/daemon/work-queue-worker.ts","../src/daemon/daemon/handler.ts","../src/daemon/daemon/server.ts","../src/daemon/daemon.ts"],"sourcesContent":["/**\n * config.ts — Notification config persistence helpers\n *\n * Reads and writes the `notifications` section of ~/.config/pai/config.json.\n * Deep-merges with defaults so partial configs work fine.\n *\n * This module is intentionally separate from the daemon's config loader\n * so it can be used standalone (e.g. from CLI commands).\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n} from \"node:fs\";\nimport {\n CONFIG_FILE,\n CONFIG_DIR,\n expandHome,\n} from \"../daemon/config.js\";\nimport type {\n NotificationConfig,\n ChannelConfigs,\n RoutingTable,\n NotificationMode,\n} from \"./types.js\";\nimport {\n DEFAULT_NOTIFICATION_CONFIG,\n DEFAULT_CHANNELS,\n DEFAULT_ROUTING,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Deep merge helper (same approach as daemon/config.ts)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Load\n// ---------------------------------------------------------------------------\n\n/**\n * Load the notification config from the PAI config file.\n * Returns defaults merged with any stored values.\n */\nexport function loadNotificationConfig(): NotificationConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n const stored = parsed[\"notifications\"];\n if (!stored || typeof stored !== \"object\") {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n return deepMerge(\n DEFAULT_NOTIFICATION_CONFIG,\n stored as Record<string, unknown>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Save\n// ---------------------------------------------------------------------------\n\n/**\n * Persist the notification config by merging it into the existing\n * ~/.config/pai/config.json. Creates the file if it does not exist.\n */\nexport function saveNotificationConfig(config: NotificationConfig): void {\n // Ensure the config dir exists\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n\n // Read current full config\n let full: Record<string, unknown> = {};\n if (existsSync(CONFIG_FILE)) {\n try {\n full = JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")) as Record<\n string,\n unknown\n >;\n } catch {\n // Start fresh if the file is unreadable\n }\n }\n\n // Replace the notifications section\n full[\"notifications\"] = config;\n\n writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ---------------------------------------------------------------------------\n// Patch helpers (used by the set command)\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a partial update to the current notification config and persist it.\n * Returns the new merged config.\n */\nexport function patchNotificationConfig(patch: {\n mode?: NotificationMode;\n channels?: Partial<Partial<ChannelConfigs>>;\n routing?: Partial<RoutingTable>;\n}): NotificationConfig {\n const current = loadNotificationConfig();\n\n if (patch.mode !== undefined) {\n current.mode = patch.mode;\n }\n\n if (patch.channels) {\n current.channels = deepMerge(\n current.channels,\n patch.channels as Record<string, unknown>\n );\n }\n\n if (patch.routing) {\n current.routing = deepMerge(\n current.routing,\n patch.routing as Record<string, unknown>\n );\n }\n\n saveNotificationConfig(current);\n return current;\n}\n\n// Re-export defaults for convenience\nexport { DEFAULT_NOTIFICATION_CONFIG, DEFAULT_CHANNELS, DEFAULT_ROUTING };\nexport { expandHome };\n","/**\n * Index, embed, and vault index schedulers for the PAI daemon.\n * Exports run* functions (also called on-demand by the IPC handler)\n * and the start* functions invoked once at daemon startup.\n */\n\nimport { indexAll } from \"../../memory/indexer.js\";\nimport type { SQLiteBackendWithDb } from \"./types.js\";\nimport {\n registryDb,\n storageBackend,\n daemonConfig,\n indexInProgress,\n embedInProgress,\n vaultIndexInProgress,\n shutdownRequested,\n setIndexInProgress,\n setLastIndexTime,\n setIndexSchedulerTimer,\n setEmbedInProgress,\n setLastEmbedTime,\n setEmbedSchedulerTimer,\n setVaultIndexInProgress,\n setLastVaultIndexTime,\n} from \"./state.js\";\n\n// ---------------------------------------------------------------------------\n// Index scheduler\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between vault index runs (30 minutes). */\nconst VAULT_INDEX_MIN_INTERVAL_MS = 30 * 60 * 1000;\n\n/**\n * Run a full index pass. Guards against overlapping runs with indexInProgress.\n * Called both by the scheduler and by the index_now IPC method.\n */\nexport async function runIndex(): Promise<void> {\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index already in progress, skipping.\\n\");\n return;\n }\n\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed in progress, deferring index run.\\n\");\n return;\n }\n\n setIndexInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled index run...\\n\");\n\n if (storageBackend.backendType === \"sqlite\") {\n const { SQLiteBackend } = await import(\"../../storage/sqlite.js\");\n if (storageBackend instanceof SQLiteBackend) {\n const db = (storageBackend as SQLiteBackendWithDb).getRawDb();\n const { projects, result } = await indexAll(db, registryDb);\n const elapsed = Date.now() - t0;\n setLastIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Index complete: ${projects} projects, ` +\n `${result.filesProcessed} files, ${result.chunksCreated} chunks ` +\n `(${elapsed}ms)\\n`\n );\n }\n } else {\n const { indexAllWithBackend } = await import(\"../../memory/indexer-backend.js\");\n const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);\n const elapsed = Date.now() - t0;\n setLastIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Index complete (postgres): ${projects} projects, ` +\n `${result.filesProcessed} files, ${result.chunksCreated} chunks ` +\n `(${elapsed}ms)\\n`\n );\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Index error: ${msg}\\n`);\n } finally {\n setIndexInProgress(false);\n }\n}\n\n/**\n * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.\n * Skips if no vaultPath is configured, or if project index/embed is in progress.\n */\nexport async function runVaultIndex(): Promise<void> {\n if (!daemonConfig.vaultPath) return;\n\n if (vaultIndexInProgress) {\n process.stderr.write(\"[pai-daemon] Vault index already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Index/embed in progress, deferring vault index.\\n\");\n return;\n }\n\n // Import lastVaultIndexTime from state (re-read each call since it may change)\n const { lastVaultIndexTime } = await import(\"./state.js\");\n if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) {\n return;\n }\n\n let vaultProjectId = daemonConfig.vaultProjectId;\n if (!vaultProjectId) {\n const row = registryDb\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(daemonConfig.vaultPath) as { id: number } | undefined;\n vaultProjectId = row?.id ?? 999;\n if (!row) {\n process.stderr.write(\"[pai-daemon] Vault not in project registry — using synthetic project ID 999.\\n\");\n }\n }\n\n setVaultIndexInProgress(true);\n const t0 = Date.now();\n\n process.stderr.write(\"[pai-daemon] Starting vault index run...\\n\");\n\n try {\n const { indexVault } = await import(\"../../memory/vault-indexer.js\");\n const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath!);\n const elapsed = Date.now() - t0;\n setLastVaultIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Vault index complete: ${r.filesIndexed} files, ` +\n `${r.linksExtracted} links, ${r.deadLinksFound} dead, ` +\n `${r.orphansFound} orphans (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Vault index error: ${msg}\\n`);\n } finally {\n setVaultIndexInProgress(false);\n }\n}\n\n/**\n * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.\n */\nexport function startIndexScheduler(): void {\n const intervalMs = daemonConfig.indexIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\\n`\n );\n\n setTimeout(() => {\n runIndex()\n .then(() => runVaultIndex())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Startup index error: ${e}\\n`);\n });\n }, 2_000);\n\n const timer = setInterval(() => {\n runIndex()\n .then(() => runVaultIndex())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled index error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setIndexSchedulerTimer(timer);\n}\n\n// ---------------------------------------------------------------------------\n// Embed scheduler\n// ---------------------------------------------------------------------------\n\n/**\n * Run an embedding pass for all unembedded chunks (Postgres backend only).\n */\nexport async function runEmbed(): Promise<void> {\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index in progress, deferring embed pass.\\n\");\n return;\n }\n\n if (storageBackend.backendType !== \"postgres\") {\n return;\n }\n\n setEmbedInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled embed pass...\\n\");\n\n const projectNames = new Map<number, string>();\n try {\n const rows = registryDb\n .prepare(\"SELECT id, slug FROM projects WHERE status = 'active'\")\n .all() as Array<{ id: number; slug: string }>;\n for (const r of rows) projectNames.set(r.id, r.slug);\n } catch { /* registry unavailable — IDs will be used instead */ }\n\n const { embedChunksWithBackend } = await import(\"../../memory/indexer-backend.js\");\n const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames);\n\n let vaultEmbedCount = 0;\n if (daemonConfig.vaultPath) {\n try {\n const { SQLiteBackend } = await import(\"../../storage/sqlite.js\");\n const { openFederation } = await import(\"../../memory/db.js\");\n const federationDb = openFederation();\n const vaultSqliteBackend = new SQLiteBackend(federationDb);\n\n const vaultProjectNames = new Map(projectNames);\n if (!vaultProjectNames.has(999)) {\n vaultProjectNames.set(999, \"obsidian-vault\");\n }\n\n vaultEmbedCount = await embedChunksWithBackend(\n vaultSqliteBackend,\n () => shutdownRequested,\n vaultProjectNames,\n );\n\n try { federationDb.close(); } catch { /* ignore */ }\n\n if (vaultEmbedCount > 0) {\n process.stderr.write(\n `[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\\n`\n );\n }\n } catch (ve) {\n const vmsg = ve instanceof Error ? ve.message : String(ve);\n process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\\n`);\n }\n }\n\n const elapsed = Date.now() - t0;\n setLastEmbedTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Embed error: ${msg}\\n`);\n } finally {\n setEmbedInProgress(false);\n }\n}\n\n/**\n * Start the periodic embed scheduler. Initial run is 60 seconds after startup.\n */\nexport function startEmbedScheduler(): void {\n const intervalMs = daemonConfig.embedIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\\n`\n );\n\n setTimeout(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Startup embed error: ${e}\\n`);\n });\n }, 60_000);\n\n const timer = setInterval(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setEmbedSchedulerTimer(timer);\n}\n","/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via Whazaa MCP)\n *\n * Sends notifications via the Whazaa Unix Domain Socket IPC protocol.\n * Falls back gracefully if Whazaa is not running.\n *\n * Whazaa IPC socket: /tmp/whazaa.sock (standard Whazaa path)\n *\n * We use the same connect-per-call pattern as PaiClient to avoid\n * requiring any persistent connection state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nconst WHAZAA_SOCKET = \"/tmp/whazaa.sock\";\nconst WHAZAA_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the Whazaa socket.\n * Returns true on success, false if Whazaa is not available or errors.\n */\nfunction callWhazaa(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(WHAZAA_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown };\n finish(!resp.error);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), WHAZAA_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n\n const params: Record<string, unknown> = {\n message: payload.message,\n };\n\n if (cfg.recipient) {\n params.recipient = cfg.recipient;\n }\n\n if (isVoiceMode && config.mode === \"voice\") {\n const voiceName = config.channels.voice.voiceName ?? \"bm_george\";\n params.voice = voiceName;\n }\n\n return callWhazaa(\"whatsapp_send\", params);\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n","/**\n * store.ts — PostgreSQL persistence for PAI observations.\n *\n * All functions accept a pg.Pool and are safe to call concurrently.\n * Schema is initialized lazily via ensureObservationTables().\n *\n * Content-hash deduplication: observations with the same hash\n * created within a 30-second window are silently dropped to prevent\n * duplicate entries from rapid repeated tool calls.\n */\n\nimport { sha256 } from '../utils/hash.js';\nimport type { Pool } from 'pg';\nimport type { ClassifiedObservation } from './classifier.js';\n\n// ---------------------------------------------------------------------------\n// Row types\n// ---------------------------------------------------------------------------\n\nexport interface ObservationRow {\n id: number;\n session_id: string;\n project_id: number | null;\n project_slug: string | null;\n type: string;\n title: string;\n narrative: string | null;\n tool_name: string | null;\n tool_input_summary: string | null;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n content_hash: string | null;\n created_at: Date;\n}\n\nexport interface SessionSummaryRow {\n id: number;\n session_id: string;\n project_id: number | null;\n project_slug: string | null;\n request: string | null;\n investigated: string | null;\n learned: string | null;\n completed: string | null;\n next_steps: string | null;\n observation_count: number;\n created_at: Date;\n}\n\n// ---------------------------------------------------------------------------\n// Input types\n// ---------------------------------------------------------------------------\n\nexport interface StoreObservationInput extends Omit<ClassifiedObservation, 'narrative'> {\n session_id: string;\n project_id?: number | null;\n project_slug?: string | null;\n narrative?: string | null;\n}\n\nexport interface StoreSessionSummaryInput {\n session_id: string;\n project_id?: number | null;\n project_slug?: string | null;\n request?: string | null;\n investigated?: string | null;\n learned?: string | null;\n completed?: string | null;\n next_steps?: string | null;\n observation_count?: number;\n}\n\nexport interface QueryObservationsOptions {\n projectId?: number;\n sessionId?: string;\n type?: string;\n limit?: number;\n offset?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Schema initialisation\n// ---------------------------------------------------------------------------\n\nlet _tablesEnsured = false;\n\n/**\n * Inlined schema DDL — avoids runtime file reads that break in bundled code\n * (the bundler puts this in a shared chunk whose __dirname differs from src/).\n */\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS pai_observations (\n id SERIAL PRIMARY KEY,\n session_id TEXT NOT NULL,\n project_id INTEGER,\n project_slug TEXT,\n type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),\n title TEXT NOT NULL,\n narrative TEXT,\n tool_name TEXT,\n tool_input_summary TEXT,\n files_read JSONB DEFAULT '[]'::jsonb,\n files_modified JSONB DEFAULT '[]'::jsonb,\n concepts JSONB DEFAULT '[]'::jsonb,\n content_hash TEXT,\n created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);\nCREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);\nCREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);\nCREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);\n\nCREATE TABLE IF NOT EXISTS pai_session_summaries (\n id SERIAL PRIMARY KEY,\n session_id TEXT NOT NULL UNIQUE,\n project_id INTEGER,\n project_slug TEXT,\n request TEXT,\n investigated TEXT,\n learned TEXT,\n completed TEXT,\n next_steps TEXT,\n observation_count INTEGER DEFAULT 0,\n created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);\nCREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);\n`;\n\n/**\n * Run schema DDL idempotently against the given pool.\n * Uses a module-level flag so subsequent calls are no-ops within the same\n * process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).\n */\nexport async function ensureObservationTables(pool: Pool): Promise<void> {\n if (_tablesEnsured) return;\n await pool.query(SCHEMA_SQL);\n _tablesEnsured = true;\n}\n\n// ---------------------------------------------------------------------------\n// Content-hash deduplication\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a 16-character hex content hash for deduplication.\n * Hash = SHA256(session_id + tool_name + title).slice(0, 16)\n */\nfunction computeContentHash(sessionId: string, toolName: string, title: string): string {\n return sha256(sessionId + '\\x00' + toolName + '\\x00' + title).slice(0, 16);\n}\n\n// ---------------------------------------------------------------------------\n// Store observation\n// ---------------------------------------------------------------------------\n\n/**\n * Insert an observation, skipping duplicates within a 30-second window.\n * Returns the inserted row's id, or null if the insert was suppressed.\n */\nexport async function storeObservation(\n pool: Pool,\n obs: StoreObservationInput\n): Promise<number | null> {\n await ensureObservationTables(pool);\n\n const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);\n\n // Check for a recent duplicate (30-second window)\n const dupCheck = await pool.query<{ id: number }>(\n `SELECT id FROM pai_observations\n WHERE content_hash = $1\n AND session_id = $2\n AND created_at >= NOW() - INTERVAL '30 seconds'\n LIMIT 1`,\n [hash, obs.session_id]\n );\n\n if (dupCheck.rowCount && dupCheck.rowCount > 0) {\n // Duplicate within dedup window — silently skip\n return null;\n }\n\n const result = await pool.query<{ id: number }>(\n `INSERT INTO pai_observations\n (session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)\n RETURNING id`,\n [\n obs.session_id,\n obs.project_id ?? null,\n obs.project_slug ?? null,\n obs.type,\n obs.title,\n obs.narrative ?? null,\n obs.tool_name,\n obs.tool_input_summary ?? null,\n JSON.stringify(obs.files_read),\n JSON.stringify(obs.files_modified),\n JSON.stringify(obs.concepts),\n hash,\n ]\n );\n\n return result.rows[0]?.id ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Query observations\n// ---------------------------------------------------------------------------\n\n/**\n * Filtered query for observations with optional projectId, sessionId, type,\n * limit, and offset. Returns results ordered by created_at DESC.\n */\nexport async function queryObservations(\n pool: Pool,\n opts: QueryObservationsOptions = {}\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n let idx = 1;\n\n if (opts.projectId !== undefined) {\n conditions.push(`project_id = $${idx++}`);\n params.push(opts.projectId);\n }\n if (opts.sessionId !== undefined) {\n conditions.push(`session_id = $${idx++}`);\n params.push(opts.sessionId);\n }\n if (opts.type !== undefined) {\n conditions.push(`type = $${idx++}`);\n params.push(opts.type);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = opts.limit ?? 50;\n const offset = opts.offset ?? 0;\n\n params.push(limit, offset);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n ${where}\n ORDER BY created_at DESC\n LIMIT $${idx++} OFFSET $${idx}`,\n params\n );\n\n return result.rows;\n}\n\n/**\n * Most recent observations for a project, ordered by created_at DESC.\n */\nexport async function queryRecentObservations(\n pool: Pool,\n projectId: number,\n limit: number\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n WHERE project_id = $1\n ORDER BY created_at DESC\n LIMIT $2`,\n [projectId, limit]\n );\n\n return result.rows;\n}\n\n/**\n * All observations for a specific session, ordered chronologically.\n */\nexport async function querySessionObservations(\n pool: Pool,\n sessionId: string\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n WHERE session_id = $1\n ORDER BY created_at ASC`,\n [sessionId]\n );\n\n return result.rows;\n}\n\n// ---------------------------------------------------------------------------\n// Session summaries\n// ---------------------------------------------------------------------------\n\n/**\n * Upsert a session summary. Uses ON CONFLICT on session_id so calling this\n * multiple times with updated content is safe.\n */\nexport async function storeSessionSummary(\n pool: Pool,\n summary: StoreSessionSummaryInput\n): Promise<void> {\n await ensureObservationTables(pool);\n\n await pool.query(\n `INSERT INTO pai_session_summaries\n (session_id, project_id, project_slug, request, investigated,\n learned, completed, next_steps, observation_count)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (session_id) DO UPDATE SET\n project_id = EXCLUDED.project_id,\n project_slug = EXCLUDED.project_slug,\n request = EXCLUDED.request,\n investigated = EXCLUDED.investigated,\n learned = EXCLUDED.learned,\n completed = EXCLUDED.completed,\n next_steps = EXCLUDED.next_steps,\n observation_count = EXCLUDED.observation_count`,\n [\n summary.session_id,\n summary.project_id ?? null,\n summary.project_slug ?? null,\n summary.request ?? null,\n summary.investigated ?? null,\n summary.learned ?? null,\n summary.completed ?? null,\n summary.next_steps ?? null,\n summary.observation_count ?? 0,\n ]\n );\n}\n\n/**\n * Most recent session summaries for a project, ordered by created_at DESC.\n */\nexport async function queryRecentSummaries(\n pool: Pool,\n projectId: number,\n limit: number\n): Promise<SessionSummaryRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<SessionSummaryRow>(\n `SELECT id, session_id, project_id, project_slug,\n request, investigated, learned, completed, next_steps,\n observation_count, created_at\n FROM pai_session_summaries\n WHERE project_id = $1\n ORDER BY created_at DESC\n LIMIT $2`,\n [projectId, limit]\n );\n\n return result.rows;\n}\n","/**\n * Tool dispatcher — maps IPC method names to PAI tool functions.\n */\n\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolProjectTodo,\n toolSessionRoute,\n} from \"../../mcp/tools.js\";\nimport { detectTopicShift } from \"../../topics/detector.js\";\nimport { registryDb, storageBackend, daemonConfig } from \"./state.js\";\nimport type { PostgresBackendWithPool } from \"./types.js\";\n\n/**\n * Dispatch an IPC tool call to the appropriate tool function.\n * Returns the tool result or throws on unknown/failed methods.\n */\nexport async function dispatchTool(\n method: string,\n params: Record<string, unknown>\n): Promise<unknown> {\n // Cast through unknown to satisfy TypeScript's strict overlap check on\n // Record<string, unknown> → specific param types. Runtime validation is\n // the responsibility of each tool function (they surface errors gracefully).\n const p = params as unknown;\n\n switch (method) {\n case \"memory_search\":\n return toolMemorySearch(registryDb, storageBackend, p as Parameters<typeof toolMemorySearch>[2]);\n\n case \"memory_get\":\n return toolMemoryGet(registryDb, p as Parameters<typeof toolMemoryGet>[1]);\n\n case \"project_info\":\n return toolProjectInfo(registryDb, p as Parameters<typeof toolProjectInfo>[1]);\n\n case \"project_list\":\n return toolProjectList(registryDb, p as Parameters<typeof toolProjectList>[1]);\n\n case \"session_list\":\n return toolSessionList(registryDb, p as Parameters<typeof toolSessionList>[1]);\n\n case \"registry_search\":\n return toolRegistrySearch(registryDb, p as Parameters<typeof toolRegistrySearch>[1]);\n\n case \"project_detect\":\n return toolProjectDetect(registryDb, p as Parameters<typeof toolProjectDetect>[1]);\n\n case \"project_health\":\n return toolProjectHealth(registryDb, p as Parameters<typeof toolProjectHealth>[1]);\n\n case \"project_todo\":\n return toolProjectTodo(registryDb, p as Parameters<typeof toolProjectTodo>[1]);\n\n case \"topic_check\":\n return detectTopicShift(\n registryDb,\n storageBackend,\n p as Parameters<typeof detectTopicShift>[2]\n );\n\n case \"session_auto_route\":\n return toolSessionRoute(\n registryDb,\n storageBackend,\n p as Parameters<typeof toolSessionRoute>[2]\n );\n\n case \"zettel_explore\":\n case \"zettel_health\":\n case \"zettel_surprise\":\n case \"zettel_suggest\":\n case \"zettel_converse\":\n case \"zettel_themes\":\n case \"zettel_god_notes\":\n case \"zettel_communities\": {\n const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import(\"../../mcp/tools.js\");\n\n switch (method) {\n case \"zettel_explore\": return toolZettelExplore(storageBackend, p as Parameters<typeof toolZettelExplore>[1]);\n case \"zettel_health\": return toolZettelHealth(storageBackend, p as Parameters<typeof toolZettelHealth>[1]);\n case \"zettel_surprise\": return toolZettelSurprise(storageBackend, p as Parameters<typeof toolZettelSurprise>[1]);\n case \"zettel_suggest\": return toolZettelSuggest(storageBackend, p as Parameters<typeof toolZettelSuggest>[1]);\n case \"zettel_converse\": return toolZettelConverse(storageBackend, p as Parameters<typeof toolZettelConverse>[1]);\n case \"zettel_themes\": return toolZettelThemes(storageBackend, p as Parameters<typeof toolZettelThemes>[1]);\n case \"zettel_god_notes\": return toolZettelGodNotes(storageBackend, p as Parameters<typeof toolZettelGodNotes>[1]);\n case \"zettel_communities\": return toolZettelCommunities(storageBackend, p as Parameters<typeof toolZettelCommunities>[1]);\n }\n break;\n }\n\n case \"graph_clusters\": {\n const { handleGraphClusters } = await import(\"../../graph/clusters.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphClusters(pgPool, storageBackend, p as Parameters<typeof handleGraphClusters>[2]);\n }\n\n case \"graph_neighborhood\": {\n const { handleGraphNeighborhood } = await import(\"../../graph/neighborhood.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphNeighborhood(pgPool, storageBackend, p as Parameters<typeof handleGraphNeighborhood>[2]);\n }\n\n case \"graph_note_context\": {\n const { handleGraphNoteContext } = await import(\"../../graph/note-context.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphNoteContext(pgPool, storageBackend, p as Parameters<typeof handleGraphNoteContext>[2]);\n }\n\n case \"graph_trace\": {\n const { handleGraphTrace } = await import(\"../../graph/trace.js\");\n return handleGraphTrace(storageBackend, p as Parameters<typeof handleGraphTrace>[1]);\n }\n\n case \"graph_latent_ideas\": {\n const { handleGraphLatentIdeas } = await import(\"../../graph/latent-ideas.js\");\n return handleGraphLatentIdeas(storageBackend, p as Parameters<typeof handleGraphLatentIdeas>[1]);\n }\n\n case \"idea_materialize\": {\n const { handleIdeaMaterialize } = await import(\"../../graph/latent-ideas.js\");\n if (!daemonConfig.vaultPath) {\n throw new Error(\"idea_materialize requires vaultPath to be configured in the daemon config\");\n }\n return handleIdeaMaterialize(\n p as Parameters<typeof handleIdeaMaterialize>[0],\n daemonConfig.vaultPath\n );\n }\n\n default:\n throw new Error(`Unknown method: ${method}`);\n }\n}\n","/**\n * work-queue.ts — Persistent work queue for the PAI Daemon\n *\n * Provides a durable, file-backed queue that survives daemon restarts.\n * Items are processed sequentially to avoid concurrent writes to the same\n * session note. Failed items are retried with exponential backoff.\n *\n * Queue file: ~/.config/pai/work-queue.json\n * Written atomically (write temp → rename) to prevent corruption.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n renameSync,\n mkdirSync,\n statSync,\n} from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type WorkItemType =\n | \"session-end\"\n | \"session-summary\"\n | \"note-update\"\n | \"todo-update\"\n | \"topic-detect\";\n\nexport type WorkItemStatus =\n | \"pending\"\n | \"processing\"\n | \"completed\"\n | \"failed\";\n\nexport interface WorkItem {\n id: string;\n type: WorkItemType;\n priority: number; // 1=high, 5=low\n payload: Record<string, unknown>;\n status: WorkItemStatus;\n createdAt: string; // ISO timestamp\n attempts: number;\n maxAttempts: number; // default 3\n nextRetryAt?: string; // ISO timestamp — undefined means ready now\n error?: string; // last error message\n completedAt?: string; // ISO timestamp\n}\n\nexport interface WorkQueueStats {\n pending: number;\n processing: number;\n completed: number;\n failed: number;\n total: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst QUEUE_FILE = join(homedir(), \".config\", \"pai\", \"work-queue.json\");\nconst MAX_QUEUE_SIZE = 1000;\nconst MAX_QUEUE_FILE_BYTES = 1024 * 1024; // 1 MB\nconst COMPLETED_TTL_MS = 60 * 60 * 1000; // 1 hour\nconst FAILED_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n/** Backoff delays in ms by attempt number (0-indexed). */\nconst BACKOFF_MS = [\n 5_000, // attempt 1 → wait 5 s\n 30_000, // attempt 2 → wait 30 s\n 300_000, // attempt 3 → wait 5 min\n];\n\n// ---------------------------------------------------------------------------\n// In-memory state\n// ---------------------------------------------------------------------------\n\nlet _queue: WorkItem[] = [];\nlet _dirty = false;\n\n// ---------------------------------------------------------------------------\n// Persistence helpers\n// ---------------------------------------------------------------------------\n\n/** Load queue from disk. Call once at daemon startup. */\nexport function loadQueue(): void {\n if (!existsSync(QUEUE_FILE)) {\n _queue = [];\n return;\n }\n\n try {\n const raw = readFileSync(QUEUE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as WorkItem[];\n if (!Array.isArray(parsed)) {\n process.stderr.write(\"[work-queue] Invalid queue file format — starting empty.\\n\");\n _queue = [];\n return;\n }\n\n // On restart, reset any 'processing' items back to 'pending' — they\n // were interrupted mid-flight and need to be retried.\n _queue = parsed.map((item) => {\n if (item.status === \"processing\") {\n return { ...item, status: \"pending\" as WorkItemStatus };\n }\n return item;\n });\n\n const stats = getStats();\n process.stderr.write(\n `[work-queue] Loaded ${_queue.length} items from disk ` +\n `(pending=${stats.pending}, failed=${stats.failed}).\\n`\n );\n } catch (e) {\n process.stderr.write(`[work-queue] Could not load queue file: ${e}\\n`);\n _queue = [];\n }\n}\n\n/** Persist queue to disk atomically. */\nexport function saveQueue(): void {\n const dir = dirname(QUEUE_FILE);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const tmpFile = QUEUE_FILE + \".tmp\";\n try {\n writeFileSync(tmpFile, JSON.stringify(_queue, null, 2), \"utf-8\");\n renameSync(tmpFile, QUEUE_FILE);\n _dirty = false;\n } catch (e) {\n process.stderr.write(`[work-queue] Could not persist queue: ${e}\\n`);\n }\n}\n\n/** Persist only if there are unsaved changes. */\nfunction saveIfDirty(): void {\n if (_dirty) saveQueue();\n}\n\n// ---------------------------------------------------------------------------\n// Queue management\n// ---------------------------------------------------------------------------\n\n/**\n * Enforce the maximum queue size cap.\n * Strategy: first drop oldest completed, then oldest low-priority pending.\n */\nfunction enforceMaxSize(): void {\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n const excess = _queue.length - MAX_QUEUE_SIZE;\n\n // Step 1: drop oldest completed items\n const completed = _queue\n .filter((i) => i.status === \"completed\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n const toDropCompleted = completed.slice(0, excess);\n const dropIds = new Set(toDropCompleted.map((i) => i.id));\n _queue = _queue.filter((i) => !dropIds.has(i.id));\n\n if (_queue.length <= MAX_QUEUE_SIZE) return;\n\n // Step 2: drop oldest low-priority pending items (priority 4-5)\n const remainingExcess = _queue.length - MAX_QUEUE_SIZE;\n const lowPriorityPending = _queue\n .filter((i) => i.status === \"pending\" && i.priority >= 4)\n .sort((a, b) => a.priority - b.priority || a.createdAt.localeCompare(b.createdAt));\n\n const toDropLow = lowPriorityPending.slice(0, remainingExcess);\n const dropLowIds = new Set(toDropLow.map((i) => i.id));\n _queue = _queue.filter((i) => !dropLowIds.has(i.id));\n\n process.stderr.write(\n `[work-queue] Pruned queue to ${_queue.length} items (cap=${MAX_QUEUE_SIZE}).\\n`\n );\n}\n\n/**\n * Add a new work item to the queue.\n * Returns the created WorkItem.\n */\nexport function enqueue(params: {\n type: WorkItemType;\n priority?: number;\n payload: Record<string, unknown>;\n maxAttempts?: number;\n}): WorkItem {\n const item: WorkItem = {\n id: randomUUID(),\n type: params.type,\n priority: params.priority ?? 3,\n payload: params.payload,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n attempts: 0,\n maxAttempts: params.maxAttempts ?? 3,\n };\n\n _queue.push(item);\n enforceMaxSize();\n _dirty = true;\n saveIfDirty();\n\n process.stderr.write(\n `[work-queue] Enqueued ${item.type} (id=${item.id}, priority=${item.priority}).\\n`\n );\n\n return item;\n}\n\n/**\n * Pick the next pending item that is ready to process (respects nextRetryAt).\n * Returns null if no eligible item exists.\n * Highest priority (lowest number) is processed first; ties broken by createdAt.\n */\nexport function dequeue(): WorkItem | null {\n const now = new Date().toISOString();\n\n const eligible = _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n });\n\n if (eligible.length === 0) return null;\n\n const item = eligible[0];\n item.status = \"processing\";\n item.attempts += 1;\n _dirty = true;\n saveIfDirty();\n\n return item;\n}\n\n/** Peek at the next eligible pending item without changing its status. */\nexport function peek(): WorkItem | null {\n const now = new Date().toISOString();\n\n return (\n _queue\n .filter((i) => {\n if (i.status !== \"pending\") return false;\n if (i.nextRetryAt && i.nextRetryAt > now) return false;\n return true;\n })\n .sort((a, b) => {\n if (a.priority !== b.priority) return a.priority - b.priority;\n return a.createdAt.localeCompare(b.createdAt);\n })[0] ?? null\n );\n}\n\n/**\n * Mark an item as completed.\n */\nexport function markCompleted(id: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n item.status = \"completed\";\n item.completedAt = new Date().toISOString();\n item.error = undefined;\n _dirty = true;\n saveIfDirty();\n}\n\n/**\n * Mark an item as failed.\n * If attempts < maxAttempts, schedules a retry with exponential backoff.\n * Otherwise, leaves status as 'failed'.\n */\nexport function markFailed(id: string, errorMsg: string): void {\n const item = _queue.find((i) => i.id === id);\n if (!item) return;\n\n item.error = errorMsg;\n\n if (item.attempts < item.maxAttempts) {\n const backoffMs = BACKOFF_MS[item.attempts - 1] ?? BACKOFF_MS[BACKOFF_MS.length - 1];\n item.status = \"pending\";\n item.nextRetryAt = new Date(Date.now() + backoffMs).toISOString();\n process.stderr.write(\n `[work-queue] Item ${id} failed (attempt ${item.attempts}/${item.maxAttempts}), ` +\n `retry in ${backoffMs / 1000}s: ${errorMsg}\\n`\n );\n } else {\n item.status = \"failed\";\n process.stderr.write(\n `[work-queue] Item ${id} exhausted retries (${item.maxAttempts} attempts): ${errorMsg}\\n`\n );\n }\n\n _dirty = true;\n saveIfDirty();\n}\n\n// ---------------------------------------------------------------------------\n// Stats\n// ---------------------------------------------------------------------------\n\nexport function getStats(): WorkQueueStats {\n const stats: WorkQueueStats = {\n pending: 0,\n processing: 0,\n completed: 0,\n failed: 0,\n total: _queue.length,\n };\n for (const item of _queue) {\n stats[item.status as keyof Omit<WorkQueueStats, \"total\">]++;\n }\n return stats;\n}\n\n// ---------------------------------------------------------------------------\n// Housekeeping\n// ---------------------------------------------------------------------------\n\n/**\n * Remove completed and permanently-failed items older than their TTL.\n * Also force-cleans all completed items if the queue file exceeds 1 MB.\n */\nexport function cleanup(): void {\n const now = Date.now();\n const before = _queue.length;\n\n // Check file size for force-clean\n let forceCleanCompleted = false;\n try {\n if (existsSync(QUEUE_FILE)) {\n const { size } = statSync(QUEUE_FILE);\n if (size > MAX_QUEUE_FILE_BYTES) {\n forceCleanCompleted = true;\n process.stderr.write(\n `[work-queue] Queue file exceeds 1 MB (${size} bytes) — force-cleaning completed items.\\n`\n );\n }\n }\n } catch {\n // non-fatal\n }\n\n _queue = _queue.filter((item) => {\n if (item.status === \"completed\") {\n if (forceCleanCompleted) return false;\n const completedMs = item.completedAt ? new Date(item.completedAt).getTime() : 0;\n return now - completedMs < COMPLETED_TTL_MS;\n }\n if (item.status === \"failed\") {\n const createdMs = new Date(item.createdAt).getTime();\n return now - createdMs < FAILED_TTL_MS;\n }\n return true;\n });\n\n const removed = before - _queue.length;\n const stats = getStats();\n\n if (removed > 0 || before === 0) {\n process.stderr.write(\n `[work-queue] Cleanup: removed ${removed} items. ` +\n `Queue stats: pending=${stats.pending}, processing=${stats.processing}, ` +\n `completed=${stats.completed}, failed=${stats.failed}.\\n`\n );\n }\n\n _dirty = removed > 0;\n saveIfDirty();\n}\n","/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n","/**\n * Path utilities — encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory — checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists → use it (don't create anything new)\n * - If no local Notes/ → ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} → sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md — check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md — returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n","/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month → previous month → flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} → ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note — mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n","/**\n * TODO.md management — creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { findTodoPath } from './paths.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp at the end.\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks).\n * Ensures exactly ONE timestamp line at the end.\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n * Mirrors \"pause session\" behavior — gives the next session a starting point.\n * Replaces any existing ## Continue section.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string\n): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove existing ## Continue section\n content = content.replace(/## Continue\\n[\\s\\S]*?\\n---\\n+/, '');\n\n const now = new Date().toISOString();\n const stateLines = state\n ? state.split('\\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\\n')\n : `> Working directory: ${cwd}. Check the latest session note for details.`;\n\n const continueSection = `## Continue\n\n> **Last session:** ${noteFilename.replace('.md', '')}\n> **Paused at:** ${now}\n>\n${stateLines}\n\n---\n\n`;\n\n content = content.replace(/^\\s+/, '');\n\n const titleMatch = content.match(/^(# [^\\n]+\\n+)/);\n if (titleMatch) {\n content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);\n } else {\n content = continueSection + content;\n }\n\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error('TODO.md ## Continue section updated');\n}\n","/**\n * session-summary-prompt.ts — Prompt template for AI-powered session summaries\n *\n * Produces a prompt that instructs the summarizer model to generate a structured\n * session note from extracted user messages and git commits. The output format\n * matches PAI's existing session note structure (Reconstruct skill format).\n *\n * The prompt also requests a TOPIC line on the first line of output, which the\n * session-summary-worker uses to detect topic shifts and decide whether to\n * create a new note or update the existing one.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SummaryPromptParams {\n /** Extracted user messages from the JSONL transcript. */\n userMessages: string[];\n /** Git log output (--oneline --stat) for the session period. */\n gitLog: string;\n /** Working directory of the session. */\n cwd: string;\n /** ISO date string for the session. */\n date: string;\n /** Files modified during the session (from tool_use blocks). */\n filesModified?: string[];\n /** Existing session note content (if updating). */\n existingNote?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the prompt string to send to the summarizer model.\n *\n * Returns a single string suitable for piping to `claude --model <model> --print`.\n */\nexport function buildSessionSummaryPrompt(params: SummaryPromptParams): string {\n const {\n userMessages,\n gitLog,\n cwd,\n date,\n filesModified,\n existingNote,\n } = params;\n\n const userSection = userMessages.length > 0\n ? userMessages.map((m, i) => `[${i + 1}] ${m}`).join(\"\\n\\n\")\n : \"(No user messages extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const filesSection = filesModified && filesModified.length > 0\n ? filesModified.map(f => `- ${f}`).join(\"\\n\")\n : \"\";\n\n const updateInstruction = existingNote\n ? `\\nAn existing session note is provided below. Merge the new information into it,\npreserving what was already written. Add new work items and update the summary.\nDo NOT duplicate existing content.\n\nEXISTING NOTE:\n${existingNote}\n`\n : \"\";\n\n return `You are summarizing a coding session. Given the user messages and git commits below, write a session note.\n\nProject directory: ${cwd}\nDate: ${date}\n\nFocus on:\n- What problems were encountered and how they were solved\n- Key architectural decisions and their rationale\n- What was built (reference actual files and code patterns)\n- What was left unfinished or needs follow-up\n\nDo NOT include:\n- Mechanical metadata (token counts, checkpoint timestamps)\n- System messages or tool results verbatim\n- Generic descriptions — be specific about what happened\n- Markdown frontmatter or YAML headers\n${updateInstruction}\nFormat your response EXACTLY as follows (no extra text before or after):\n\nTOPIC: [A short topic label, max 60 characters, describing the WORK DONE — not quoting user messages. Format as \"Topic1, Topic2, and Topic3\" if multiple themes. Example: \"Session Summary Worker, Topic Detection\"]\n\n# Session: [Descriptive title summarizing what was ACCOMPLISHED, max 60 characters. Describe the work done, not the user's request. Bad: \"Dark Mode Button Does Nothing\". Good: \"Dark Mode Toggle, Keyboard IPC, and Audio Fix\"]\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n[Organize by theme, not chronologically. Group related work under descriptive bullet points.\nUse checkbox format: - [x] for completed items, - [ ] for incomplete items.\nInclude specific file names, function names, and technical details.]\n\n## Key Decisions\n\n[List important choices made during the session with brief rationale.\nSkip this section entirely if no significant decisions were made.]\n\n## Known Issues\n\n[What was left unfinished, bugs discovered, or follow-up items needed.\nSkip this section entirely if nothing is pending.]\n\n---\n\nUSER MESSAGES:\n${userSection}\n\nGIT COMMITS:\n${gitSection}\n${filesSection ? `\\nFILES MODIFIED:\\n${filesSection}` : \"\"}`;\n}\n","/**\n * session-summary-worker.ts — AI-powered session note generation\n *\n * Processes `session-summary` work items by:\n * 1. Finding the current session's JSONL transcript\n * 2. Extracting user messages and assistant context\n * 3. Gathering git commits from the session period\n * 4. Spawning Claude (sonnet for compaction, opus for session end) to generate a structured summary\n * 5. Comparing the new topic against the existing note's topic\n * 6. Creating a NEW note if the topic shifted, or updating the existing one\n *\n * Topic detection: the summarizer outputs a TOPIC: line as the first line of\n * its response. This is compared against the existing note's title using word\n * overlap. If overlap is below ~30%, a new note is created.\n *\n * Designed to run inside the daemon's work queue worker. All errors are\n * thrown (not swallowed) so the work queue retry logic handles them.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport {\n findNotesDir,\n getCurrentNotePath,\n createSessionNote,\n addWorkToSessionNote,\n renameSessionNote,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\nimport { buildSessionSummaryPrompt } from \"./templates/session-summary-prompt.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between summaries for the same project (ms). */\nconst SUMMARY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes\n\n/** Maximum JSONL content to feed to the summarizer (characters). */\n/** Max JSONL chars per model. Opus/Sonnet can handle much more than Haiku. */\nconst MAX_JSONL_CHARS: Record<string, number> = {\n haiku: 50_000,\n sonnet: 200_000,\n opus: 500_000,\n};\n\n/** Maximum user messages to include in the prompt. */\nconst MAX_USER_MESSAGES = 30;\n\n/** Timeout for the claude CLI process (ms). */\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000, // 60 seconds\n sonnet: 120_000, // 2 minutes\n opus: 300_000, // 5 minutes — opus is thorough\n};\n\n/** File tracking last summary timestamps per project. */\nconst COOLDOWN_FILE = join(homedir(), \".config\", \"pai\", \"summary-cooldowns.json\");\n\n/** Claude Code projects directory. */\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SessionSummaryPayload {\n cwd: string;\n sessionId?: string;\n projectSlug?: string;\n transcriptPath?: string;\n /** If true, bypass the cooldown check (e.g. triggered by stop-hook at session end). */\n force?: boolean;\n /** Model to use for summarization. Defaults based on trigger:\n * - \"stop\" trigger (session end): \"opus\" for best quality final summary\n * - \"compact\" trigger (auto-compaction): \"sonnet\" for incremental checkpoints\n * - Manual/reconstruct: \"sonnet\" for batch processing */\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n}\n\n// ---------------------------------------------------------------------------\n// Cooldown tracking\n// ---------------------------------------------------------------------------\n\nfunction loadCooldowns(): Record<string, number> {\n try {\n if (existsSync(COOLDOWN_FILE)) {\n return JSON.parse(readFileSync(COOLDOWN_FILE, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nfunction saveCooldowns(cooldowns: Record<string, number>): void {\n try {\n writeFileSync(COOLDOWN_FILE, JSON.stringify(cooldowns, null, 2), \"utf-8\");\n } catch { /* ignore */ }\n}\n\nfunction isOnCooldown(cwd: string): boolean {\n const cooldowns = loadCooldowns();\n const lastRun = cooldowns[cwd];\n if (!lastRun) return false;\n return Date.now() - lastRun < SUMMARY_COOLDOWN_MS;\n}\n\nfunction markCooldown(cwd: string): void {\n const cooldowns = loadCooldowns();\n cooldowns[cwd] = Date.now();\n // Prune entries older than 24 hours\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const key of Object.keys(cooldowns)) {\n if (cooldowns[key] < cutoff) delete cooldowns[key];\n }\n saveCooldowns(cooldowns);\n}\n\n// ---------------------------------------------------------------------------\n// JSONL discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cwd path the same way Claude Code does for its project directories.\n * Replaces /, space, dot, and hyphen with -.\n */\nfunction encodeProjectPath(cwd: string): string {\n return cwd.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Find the most recently modified JSONL file for the given project.\n *\n * Claude Code stores transcripts in:\n * ~/.claude/projects/<encoded-path>/sessions/*.jsonl\n * ~/.claude/projects/<encoded-path>/<uuid>.jsonl (legacy)\n */\nfunction findLatestJsonl(cwd: string): string | null {\n const encoded = encodeProjectPath(cwd);\n const projectDir = join(CLAUDE_PROJECTS_DIR, encoded);\n\n if (!existsSync(projectDir)) {\n process.stderr.write(\n `[session-summary] No Claude project dir found: ${projectDir}\\n`\n );\n return null;\n }\n\n // Collect all JSONL candidates\n const candidates: Array<{ path: string; mtime: number }> = [];\n\n // Check sessions/ subdirectory first (current layout)\n const sessionsDir = join(projectDir, \"sessions\");\n if (existsSync(sessionsDir)) {\n try {\n for (const f of readdirSync(sessionsDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(sessionsDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n }\n\n // Also check top-level for legacy .jsonl files\n try {\n for (const f of readdirSync(projectDir)) {\n if (!f.endsWith(\".jsonl\")) continue;\n const fullPath = join(projectDir, f);\n try {\n const st = statSync(fullPath);\n candidates.push({ path: fullPath, mtime: st.mtimeMs });\n } catch { /* skip */ }\n }\n } catch { /* skip */ }\n\n if (candidates.length === 0) {\n process.stderr.write(\n `[session-summary] No JSONL files found in ${projectDir}\\n`\n );\n return null;\n }\n\n // Sort by modification time descending — pick the most recent\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates[0].path;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL content extraction\n// ---------------------------------------------------------------------------\n\ninterface ExtractedContent {\n userMessages: string[];\n filesModified: string[];\n sessionStartTime: string;\n}\n\n/**\n * Parse a JSONL transcript and extract relevant content.\n * Filters noise, truncates to model-appropriate size from the end of the file.\n */\nfunction extractFromJsonl(jsonlPath: string, model: string = \"sonnet\"): ExtractedContent {\n const result: ExtractedContent = {\n userMessages: [],\n filesModified: [],\n sessionStartTime: \"\",\n };\n\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read JSONL at ${jsonlPath}: ${e}`);\n }\n\n // Truncate from the start if too large (keep the most recent content)\n const maxChars = MAX_JSONL_CHARS[model] ?? 200_000;\n if (raw.length > maxChars) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - maxChars);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_JSONL_CHARS);\n }\n\n const lines = raw.trim().split(\"\\n\");\n const seenMessages = new Set<string>();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n // Track earliest timestamp\n if (entry.timestamp && !result.sessionStartTime) {\n result.sessionStartTime = String(entry.timestamp);\n }\n\n // Extract user messages\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && !isNoise(text) && !seenMessages.has(text)) {\n seenMessages.add(text);\n result.userMessages.push(text.slice(0, 500));\n }\n }\n }\n\n // Extract file modifications from assistant tool_use blocks\n if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content && Array.isArray(msg.content)) {\n for (const block of msg.content as Array<Record<string, unknown>>) {\n if (block.type === \"tool_use\") {\n const name = block.name as string;\n const input = block.input as Record<string, unknown> | undefined;\n if ((name === \"Edit\" || name === \"Write\") && input?.file_path) {\n const fp = String(input.file_path);\n if (!result.filesModified.includes(fp)) {\n result.filesModified.push(fp);\n }\n }\n }\n }\n }\n }\n }\n\n // Limit user messages\n if (result.userMessages.length > MAX_USER_MESSAGES) {\n result.userMessages = result.userMessages.slice(-MAX_USER_MESSAGES);\n }\n\n return result;\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n/** Filter out noise entries that shouldn't be included in the summary. */\nfunction isNoise(text: string): boolean {\n if (!text || text.length < 3) return true;\n if (text.includes(\"<task-notification>\")) return true;\n if (text.includes(\"[object Object]\")) return true;\n if (text.startsWith(\"<system-reminder>\")) return true;\n if (/^(yes|ok|sure|go|continue|weiter|thanks|thank you)\\.?$/i.test(text.trim())) return true;\n // Skip pure tool result blocks\n if (text.startsWith(\"Tool Result:\") || text.startsWith(\"tool_result\")) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Git context\n// ---------------------------------------------------------------------------\n\n/**\n * Get git log for the session period.\n * Falls back gracefully if git is not available or the dir is not a repo.\n */\nasync function getGitContext(cwd: string, sinceTime?: string): Promise<string> {\n let since = \"6 hours ago\";\n if (sinceTime) {\n // sinceTime may be a Unix epoch (seconds as string) or already ISO 8601\n const asNum = Number(sinceTime);\n if (!isNaN(asNum) && asNum > 1_000_000_000) {\n // Unix epoch seconds → ISO 8601 (git accepts this unambiguously)\n since = new Date(asNum * 1000).toISOString();\n } else {\n since = sinceTime;\n }\n }\n\n try {\n const { execFile: execFileCb } = await import(\"node:child_process\");\n const { promisify } = await import(\"node:util\");\n const execFileAsync = promisify(execFileCb);\n\n const { stdout } = await execFileAsync(\n \"git\",\n [\"log\", \"--format=%h %ai %s\", `--since=${since}`, \"--stat\", \"--no-color\"],\n {\n cwd,\n timeout: 10_000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: \"0\" },\n }\n );\n return stdout.trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Claude CLI spawning\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary.\n * Checks PATH first, then common installation locations.\n */\nfunction findClaudeBinary(): string | null {\n // Check known locations first (launchd PATH is minimal, bare \"claude\" won't resolve)\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n\n // Last resort: try bare \"claude\" in case PATH has it\n return \"claude\";\n}\n\n/**\n * Spawn a Claude model via the CLI to generate a session summary.\n * Pipes the prompt via stdin. Model selection:\n * - opus: session end (best quality for final summary, runs once)\n * - sonnet: auto-compaction (good quality for incremental checkpoints, runs often)\n * - haiku: fallback / budget mode\n * Returns the generated text, or null if spawning fails.\n */\nasync function spawnSummarizer(prompt: string, model: string = \"sonnet\"): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\n \"[session-summary] Claude CLI not found in PATH or common locations.\\n\"\n );\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n // Strip ANTHROPIC_API_KEY so claude CLI uses the Max plan (free)\n // instead of billing against the API key\n const { ANTHROPIC_API_KEY: _, ...envWithoutApiKey } = process.env;\n const child = spawn(claudeBin, [\"--model\", model, \"-p\", \"--no-session-persistence\"], {\n env: envWithoutApiKey,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[session-summary] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[session-summary] ${model} exited with code ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n // Timeout protection\n timer = setTimeout(() => {\n process.stderr.write(`[session-summary] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n // Write prompt to stdin and close\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Topic extraction and comparison\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the TOPIC: line from the summarizer output.\n * Returns the topic string, or null if not found.\n */\nfunction extractTopic(summaryText: string): string | null {\n const match = summaryText.match(/^TOPIC:\\s*(.+)$/m);\n if (!match) return null;\n return match[1].trim();\n}\n\n/**\n * Extract the topic from an existing session note.\n * First checks for a <!-- TOPIC: ... --> comment (stored by previous summaries).\n * Falls back to the H1 \"# Session NNNN: Title\" line.\n *\n * The HTML comment is the reliable source because the H1 gets renamed by\n * renameSessionNote, which can add/change words and cause false topic shifts.\n */\nfunction extractExistingNoteTitle(notePath: string): string | null {\n try {\n const content = readFileSync(notePath, \"utf-8\");\n // Prefer stored TOPIC comment (exact match to what the summarizer produced)\n const topicComment = content.match(/<!-- TOPIC:\\s*(.+?)\\s*-->/);\n if (topicComment) return topicComment[1].trim();\n // Fallback to H1\n const match = content.match(/^# Session \\d+:\\s*(.+)$/m);\n if (match) return match[1].trim();\n } catch { /* ignore */ }\n return null;\n}\n\n/**\n * Compute word overlap ratio between two topic strings.\n * Returns a value in [0, 1] — 1.0 means identical word sets.\n *\n * Uses lowercased, normalized words. Stop words and very short words\n * are excluded to avoid false positives on common terms.\n */\nfunction computeTopicOverlap(topicA: string, topicB: string): number {\n const stopWords = new Set([\n \"a\", \"an\", \"the\", \"and\", \"or\", \"but\", \"in\", \"on\", \"at\", \"to\", \"for\",\n \"of\", \"with\", \"by\", \"from\", \"is\", \"was\", \"are\", \"were\", \"be\", \"been\",\n \"being\", \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\",\n \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\", \"this\", \"that\",\n \"these\", \"those\", \"it\", \"its\", \"new\", \"session\", \"work\", \"done\",\n ]);\n\n const normalize = (text: string): Set<string> => {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !stopWords.has(w));\n return new Set(words);\n };\n\n const wordsA = normalize(topicA);\n const wordsB = normalize(topicB);\n\n if (wordsA.size === 0 || wordsB.size === 0) return 0;\n\n let intersection = 0;\n for (const w of wordsA) {\n if (wordsB.has(w)) intersection++;\n }\n\n // Jaccard similarity\n const union = new Set([...wordsA, ...wordsB]).size;\n return union > 0 ? intersection / union : 0;\n}\n\n/** Threshold: below this overlap ratio, we consider topics different. */\n// Raised from 0.3 to 0.15 — lower threshold means topics must be MORE different\n// to trigger a split. 0.3 was too aggressive: incremental work on the same project\n// (e.g., \"Flutter Rewrite\" vs \"Fix Transcription in Flutter\") was splitting into\n// separate notes on every compaction.\nconst TOPIC_OVERLAP_THRESHOLD = 0.15;\n\n// ---------------------------------------------------------------------------\n// Session note writing\n// ---------------------------------------------------------------------------\n\n/**\n * Write (or update) the session note with the AI-generated summary.\n *\n * Strategy:\n * - Find the current month's latest note\n * - If it's from today, compare topics:\n * - Same topic (overlap >= 30%) → update existing note\n * - Different topic (overlap < 30%) → create a NEW note\n * - If it's from a different day, create a new note\n */\nfunction writeSessionNote(\n cwd: string,\n summaryText: string,\n filesModified: string[]\n): string | null {\n const notesInfo = findNotesDir(cwd);\n let notePath = getCurrentNotePath(notesInfo.path);\n\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Extract the new topic from the summarizer output\n const newTopic = extractTopic(summaryText);\n\n if (notePath) {\n const noteFilename = basename(notePath);\n // Check if this note is from today\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n const noteDate = dateMatch ? dateMatch[1] : \"\";\n\n if (noteDate === today) {\n // Check for topic shift — two signals:\n // 1. TOPIC: line from summarizer vs existing note title (word overlap)\n // 2. topic-boundary.json written by topic-detect-worker (project-level shift)\n const existingTitle = extractExistingNoteTitle(notePath);\n let topicShifted = false;\n\n // Signal 1: summarizer topic vs existing note title\n if (newTopic && existingTitle) {\n const overlap = computeTopicOverlap(newTopic, existingTitle);\n process.stderr.write(\n `[session-summary] Topic overlap: ${(overlap * 100).toFixed(1)}%` +\n ` (new=\"${newTopic}\", existing=\"${existingTitle}\")\\n`\n );\n\n if (overlap < TOPIC_OVERLAP_THRESHOLD) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (word overlap) — creating new note.\\n`\n );\n }\n }\n\n // Signal 2: topic boundary marker from topic-detect-worker\n if (!topicShifted) {\n const boundaryPath = join(notesInfo.path, \"topic-boundary.json\");\n if (existsSync(boundaryPath)) {\n try {\n const boundary = JSON.parse(readFileSync(boundaryPath, \"utf-8\"));\n if (boundary.timestamp) {\n const boundaryAge = Date.now() - new Date(boundary.timestamp).getTime();\n // Only honor boundaries from the last 30 minutes\n if (boundaryAge < 30 * 60 * 1000) {\n topicShifted = true;\n process.stderr.write(\n `[session-summary] Topic shift detected (boundary marker) — ` +\n `${boundary.previousProject} → ${boundary.suggestedProject}\\n`\n );\n }\n }\n // Consume the boundary file (one-shot)\n unlinkSync(boundaryPath);\n } catch { /* ignore invalid boundary file */ }\n }\n }\n\n if (topicShifted) {\n // Different topic — create a NEW note (topic-based split)\n notePath = createNoteFromSummary(notesInfo.path, summaryText);\n } else {\n // Same topic — update existing note\n updateNoteWithSummary(notePath, summaryText);\n process.stderr.write(\n `[session-summary] Updated existing note: ${noteFilename}\\n`\n );\n }\n } else {\n // Different day — create a new note\n notePath = createNoteFromSummary(notesInfo.path, summaryText);\n }\n } else {\n // No note exists — create one\n notePath = createNoteFromSummary(notesInfo.path, summaryText);\n }\n\n // Try to rename with a meaningful title from the summary\n if (notePath) {\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n if (titleMatch) {\n const title = titleMatch[1].trim();\n if (title.length > 5 && title.length < 80) {\n const newPath = renameSessionNote(notePath, title);\n if (newPath !== notePath) {\n notePath = newPath;\n }\n }\n }\n }\n\n return notePath;\n}\n\n/**\n * Update an existing session note's Work Done section with AI-generated content.\n */\nfunction updateNoteWithSummary(notePath: string, summaryText: string): void {\n if (!existsSync(notePath)) return;\n\n let content = readFileSync(notePath, \"utf-8\");\n\n // Update the TOPIC comment if present (keeps topic comparison stable across renames)\n const newTopic = extractTopic(summaryText);\n if (newTopic) {\n if (content.includes(\"<!-- TOPIC:\")) {\n content = content.replace(/<!-- TOPIC:.*?-->/, `<!-- TOPIC: ${newTopic} -->`);\n } else {\n // Insert after H1 line\n content = content.replace(/^(# Session .+)$/m, `$1\\n<!-- TOPIC: ${newTopic} -->`);\n }\n }\n\n // Extract the work items from the AI summary\n const workDoneMatch = summaryText.match(\n /## Work Done\\n\\n([\\s\\S]*?)(?=\\n## Key Decisions|\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n\n if (workDoneMatch) {\n const aiWorkContent = workDoneMatch[1].trim();\n const timestamp = new Date().toISOString().split(\"T\")[1].split(\".\")[0];\n\n // Add as a new subsection under Work Done\n const sectionHeader = `\\n### AI Summary (${timestamp})\\n\\n${aiWorkContent}\\n`;\n\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const knownIssuesIdx = content.indexOf(\"## Known Issues\");\n const insertBefore = knownIssuesIdx !== -1 ? knownIssuesIdx :\n nextStepsIdx !== -1 ? nextStepsIdx :\n content.length;\n\n content = content.slice(0, insertBefore) + sectionHeader + \"\\n\" + content.slice(insertBefore);\n }\n\n // Extract and add Key Decisions if present\n const decisionsMatch = summaryText.match(\n /## Key Decisions\\n\\n([\\s\\S]*?)(?=\\n## Known Issues|\\n\\*\\*Tags|\\n$)/\n );\n if (decisionsMatch) {\n const decisions = decisionsMatch[1].trim();\n if (decisions && !content.includes(\"## Key Decisions\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Key Decisions\\n\\n${decisions}\\n\\n` + content.slice(insertAt);\n }\n }\n\n // Extract and add Known Issues if present\n const issuesMatch = summaryText.match(\n /## Known Issues\\n\\n([\\s\\S]*?)(?=\\n\\*\\*Tags|\\n$)/\n );\n if (issuesMatch) {\n const issues = issuesMatch[1].trim();\n if (issues && !content.includes(\"## Known Issues\")) {\n const nextStepsIdx = content.indexOf(\"## Next Steps\");\n const insertAt = nextStepsIdx !== -1 ? nextStepsIdx : content.length;\n content = content.slice(0, insertAt) + `## Known Issues\\n\\n${issues}\\n\\n` + content.slice(insertAt);\n }\n }\n\n writeFileSync(notePath, content, \"utf-8\");\n}\n\n/**\n * Create a brand new session note from the AI summary.\n */\nfunction createNoteFromSummary(notesDir: string, summaryText: string): string | null {\n try {\n // Create the note with a placeholder title\n const notePath = createSessionNote(notesDir, \"New Session\");\n\n // We will overwrite the entire content with the AI-generated summary, preserving\n // the note number (derived from the filename) and adding the standard footer.\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : \"0000\";\n\n // Replace the H1 title from the AI summary with the numbered format\n const titleMatch = summaryText.match(/^# Session:\\s*(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"New Session\";\n\n const date = new Date().toISOString().split(\"T\")[0];\n\n // Extract topic before stripping it (stored as HTML comment for future comparison)\n const topic = extractTopic(summaryText);\n\n // Build the final note content, merging AI output with the PAI note structure.\n // Strip the TOPIC: line (used for topic detection, not for the note body).\n const aiBody = summaryText\n .replace(/^TOPIC:.*$/m, \"\")\n .replace(/^# Session:.*$/m, \"\")\n .replace(/^\\*\\*Date:\\*\\*.*$/m, \"\")\n .replace(/^\\*\\*Status:\\*\\*.*$/m, \"\")\n .replace(/^---$/m, \"\")\n .trim();\n\n const finalContent = `# Session ${noteNumber}: ${title}\n${topic ? `<!-- TOPIC: ${topic} -->` : \"\"}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n${aiBody}\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(notePath, finalContent, \"utf-8\");\n process.stderr.write(`[session-summary] Created AI-powered note: ${noteFilename}\\n`);\n return notePath;\n } catch (e) {\n process.stderr.write(`[session-summary] Failed to create note: ${e}\\n`);\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `session-summary` work item.\n *\n * This is the main function called by work-queue-worker.ts.\n * Throws on fatal errors (work queue will retry with backoff).\n */\nexport async function handleSessionSummary(payload: SessionSummaryPayload): Promise<void> {\n const { cwd, sessionId, projectSlug, transcriptPath, force } = payload;\n\n if (!cwd) {\n throw new Error(\"session-summary payload missing cwd\");\n }\n\n process.stderr.write(\n `[session-summary] Starting for ${cwd}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}` +\n `${force ? \" (force=true)\" : \"\"}\\n`\n );\n\n // -------------------------------------------------------------------------\n // Cooldown check — don't summarize too frequently\n // force=true bypasses the cooldown (used by stop-hook at session end so a\n // final summary is always produced regardless of recent PreCompact runs).\n // -------------------------------------------------------------------------\n if (!force && isOnCooldown(cwd)) {\n process.stderr.write(\n \"[session-summary] Skipping — last summary was less than 30 minutes ago.\\n\"\n );\n return;\n }\n\n // -------------------------------------------------------------------------\n // Step 1: Find the JSONL transcript\n // -------------------------------------------------------------------------\n let jsonlPath: string | null = transcriptPath || null;\n\n if (jsonlPath && !existsSync(jsonlPath)) {\n process.stderr.write(\n `[session-summary] Provided transcript path not found: ${jsonlPath}\\n`\n );\n jsonlPath = null;\n }\n\n if (!jsonlPath) {\n jsonlPath = findLatestJsonl(cwd);\n }\n\n if (!jsonlPath) {\n process.stderr.write(\n \"[session-summary] No JSONL transcript found — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(`[session-summary] Using transcript: ${jsonlPath}\\n`);\n\n // -------------------------------------------------------------------------\n // Step 2: Extract content from the JSONL\n // -------------------------------------------------------------------------\n // Model selection: opus for session end (force=true), sonnet for auto-compact\n const selectedModel = payload.model ?? (force ? \"opus\" : \"sonnet\");\n const extracted = extractFromJsonl(jsonlPath, selectedModel);\n\n if (extracted.userMessages.length === 0) {\n process.stderr.write(\n \"[session-summary] No user messages found in transcript — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[session-summary] Extracted ${extracted.userMessages.length} user messages, ` +\n `${extracted.filesModified.length} modified files.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 3: Get git context\n // -------------------------------------------------------------------------\n const gitLog = await getGitContext(cwd, extracted.sessionStartTime);\n\n if (gitLog) {\n process.stderr.write(\n `[session-summary] Got git context (${gitLog.split(\"\\n\").length} lines).\\n`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Build and send prompt to summarizer\n // -------------------------------------------------------------------------\n const today = new Date().toISOString().split(\"T\")[0];\n\n // Check for existing note to merge with\n const notesInfo = findNotesDir(cwd);\n const existingNotePath = getCurrentNotePath(notesInfo.path);\n let existingNote: string | undefined;\n\n if (existingNotePath) {\n const noteFilename = basename(existingNotePath);\n const dateMatch = noteFilename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n if (dateMatch && dateMatch[1] === today) {\n try {\n existingNote = readFileSync(existingNotePath, \"utf-8\");\n } catch { /* ignore */ }\n }\n }\n\n const prompt = buildSessionSummaryPrompt({\n userMessages: extracted.userMessages,\n gitLog,\n cwd,\n date: today,\n filesModified: extracted.filesModified,\n existingNote,\n });\n\n process.stderr.write(\n `[session-summary] Sending ${prompt.length} char prompt to ${selectedModel}...\\n`\n );\n\n const summaryText = await spawnSummarizer(prompt, selectedModel);\n\n if (!summaryText) {\n process.stderr.write(\n `[session-summary] ${selectedModel} did not produce output — falling back to mechanical checkpoint.\\n`\n );\n // Don't throw — this is a soft failure. The existing PreCompact checkpoint\n // is sufficient. Just mark the cooldown so we don't retry too soon.\n markCooldown(cwd);\n return;\n }\n\n process.stderr.write(\n `[session-summary] ${selectedModel} produced ${summaryText.length} char summary.\\n`\n );\n\n // -------------------------------------------------------------------------\n // Step 5: Write the session note\n // -------------------------------------------------------------------------\n const notePath = writeSessionNote(cwd, summaryText, extracted.filesModified);\n\n if (notePath) {\n process.stderr.write(\n `[session-summary] Session note written: ${basename(notePath)}\\n`\n );\n }\n\n // Mark cooldown\n markCooldown(cwd);\n\n process.stderr.write(\"[session-summary] Done.\\n\");\n}\n","/**\n * topic-detect-worker.ts — Topic shift detection for session note splitting\n *\n * Processes `topic-detect` work items by:\n * 1. Extracting recent user messages from the JSONL transcript\n * 2. Running the BM25-based topic shift detector against the PAI memory DB\n * 3. If a shift is detected, recording a topic boundary marker\n *\n * The actual note splitting is handled by session-summary-worker.ts when it\n * processes the next `session-summary` work item — it uses the TOPIC: line\n * from the summarizer to decide whether to create a new note.\n *\n * This worker provides an additional signal: project-level topic shift\n * (e.g., conversation moved from project A to project B). The session\n * summary worker handles intra-project topic shifts (e.g., from \"dark mode\"\n * to \"keyboard IPC\" within the same project).\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { detectTopicShift } from \"../topics/detector.js\";\nimport { registryDb, storageBackend } from \"./daemon/state.js\";\nimport {\n findNotesDir,\n getCurrentNotePath,\n appendCheckpoint,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectPayload {\n /** Working directory of the session. */\n cwd: string;\n /** Recent conversation context (extracted user messages). */\n context?: string;\n /** The project slug the session is currently routed to. */\n currentProject?: string;\n /** Path to the JSONL transcript (optional — used to extract context if not provided). */\n transcriptPath?: string;\n /** Session ID (for logging). */\n sessionId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL context extraction (lightweight — just last few user messages)\n// ---------------------------------------------------------------------------\n\nconst MAX_CONTEXT_MESSAGES = 5;\nconst MAX_CONTEXT_CHARS = 2000;\n\n/**\n * Extract recent user messages from a JSONL transcript for topic detection.\n * Takes only the last few messages to represent the current topic.\n */\nfunction extractRecentContext(jsonlPath: string): string {\n try {\n const raw = readFileSync(jsonlPath, \"utf-8\");\n // Read from the end — last 50KB should be more than enough\n const tail = raw.length > 50_000 ? raw.slice(-50_000) : raw;\n const lines = tail.trim().split(\"\\n\");\n\n const messages: string[] = [];\n\n for (let i = lines.length - 1; i >= 0 && messages.length < MAX_CONTEXT_MESSAGES; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n\n try {\n const entry = JSON.parse(line) as Record<string, unknown>;\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && text.length > 3) {\n messages.unshift(text.slice(0, 500));\n }\n }\n }\n } catch { /* skip invalid JSON */ }\n }\n\n return messages.join(\"\\n\\n\").slice(0, MAX_CONTEXT_CHARS);\n } catch {\n return \"\";\n }\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Topic boundary file — signals to session-summary-worker\n// ---------------------------------------------------------------------------\n\nconst TOPIC_BOUNDARY_FILE = \"topic-boundary.json\";\n\ninterface TopicBoundary {\n timestamp: string;\n previousProject: string | null;\n suggestedProject: string | null;\n confidence: number;\n context: string;\n}\n\n/**\n * Write a topic boundary marker into the Notes directory.\n * The session-summary-worker checks for this file and uses it as an\n * additional signal that a new note should be created.\n */\nfunction writeTopicBoundary(\n cwd: string,\n boundary: TopicBoundary\n): void {\n try {\n const notesInfo = findNotesDir(cwd);\n const boundaryPath = join(notesInfo.path, TOPIC_BOUNDARY_FILE);\n writeFileSync(boundaryPath, JSON.stringify(boundary, null, 2), \"utf-8\");\n process.stderr.write(\n `[topic-detect] Wrote topic boundary marker: ${boundaryPath}\\n`\n );\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not write boundary marker: ${e}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `topic-detect` work item.\n *\n * Called by work-queue-worker.ts. Throws on fatal errors so the work queue\n * retry logic handles them.\n */\nexport async function handleTopicDetect(payload: TopicDetectPayload): Promise<void> {\n const { cwd, currentProject, transcriptPath, sessionId } = payload;\n\n if (!cwd) {\n throw new Error(\"topic-detect payload missing cwd\");\n }\n\n process.stderr.write(\n `[topic-detect] Starting for ${cwd}` +\n `${currentProject ? ` (project=${currentProject})` : \"\"}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}\\n`\n );\n\n // Check that daemon state is available\n if (!registryDb || !storageBackend) {\n process.stderr.write(\n \"[topic-detect] Registry DB or storage backend not available — skipping.\\n\"\n );\n return;\n }\n\n // Extract context from payload or transcript\n let context = payload.context || \"\";\n\n if (!context && transcriptPath && existsSync(transcriptPath)) {\n context = extractRecentContext(transcriptPath);\n }\n\n if (!context || context.trim().length < 10) {\n process.stderr.write(\n \"[topic-detect] Insufficient context for topic detection — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[topic-detect] Context: ${context.length} chars, checking against memory...\\n`\n );\n\n // Run the BM25-based topic shift detector\n const result = await detectTopicShift(registryDb, storageBackend, {\n context,\n currentProject,\n threshold: 0.6,\n candidates: 20,\n });\n\n process.stderr.write(\n `[topic-detect] Result: shifted=${result.shifted}, ` +\n `suggested=${result.suggestedProject}, confidence=${result.confidence.toFixed(2)}, ` +\n `chunks=${result.chunkCount}\\n`\n );\n\n if (result.topProjects.length > 0) {\n process.stderr.write(\n `[topic-detect] Top projects: ${result.topProjects.map(\n (p) => `${p.slug}(${(p.score * 100).toFixed(0)}%)`\n ).join(\", \")}\\n`\n );\n }\n\n if (result.shifted) {\n // Record the topic boundary\n writeTopicBoundary(cwd, {\n timestamp: new Date().toISOString(),\n previousProject: result.currentProject,\n suggestedProject: result.suggestedProject,\n confidence: result.confidence,\n context: context.slice(0, 200),\n });\n\n // Also append a checkpoint to the current session note\n try {\n const notesInfo = findNotesDir(cwd);\n const notePath = getCurrentNotePath(notesInfo.path);\n if (notePath) {\n appendCheckpoint(\n notePath,\n `Topic shift detected: conversation moved from **${result.currentProject}** ` +\n `to **${result.suggestedProject}** (confidence: ${(result.confidence * 100).toFixed(0)}%). ` +\n `A new session note will be created for the new topic.`\n );\n }\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not append checkpoint: ${e}\\n`);\n }\n }\n\n process.stderr.write(\"[topic-detect] Done.\\n\");\n}\n","/**\n * work-queue-worker.ts — Daemon worker loop for the persistent work queue\n *\n * Runs every 5 seconds to drain the queue.\n * Handles 'session-end' work items by reading the transcript, extracting\n * work summaries, updating the session note, and updating TODO.md.\n * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.\n *\n * Handles 'topic-detect' items by running BM25-based topic shift detection.\n * Other item types (note-update, todo-update) are stubs — they log and\n * complete immediately, ready for future expansion.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\n\nimport {\n dequeue,\n markCompleted,\n markFailed,\n cleanup,\n getStats,\n type WorkItem,\n} from \"./work-queue.js\";\n\nimport {\n handleSessionSummary,\n type SessionSummaryPayload,\n} from \"./session-summary-worker.js\";\n\nimport {\n handleTopicDetect,\n type TopicDetectPayload,\n} from \"./topic-detect-worker.js\";\n\n// Hooks lib imports — resolving through the compiled JS path.\n// These are the same utilities used by stop-hook.ts.\nimport {\n findNotesDir,\n getCurrentNotePath,\n addWorkToSessionNote,\n finalizeSessionNote,\n updateTodoContinue,\n moveSessionFilesToSessionsDir,\n type WorkItem as NoteWorkItem,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst WORKER_INTERVAL_MS = 5_000;\nconst HOUSEKEEPING_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes\n\n// ---------------------------------------------------------------------------\n// Timers (stored so shutdown can clear them)\n// ---------------------------------------------------------------------------\n\nlet workerTimer: ReturnType<typeof setInterval> | null = null;\nlet housekeepingTimer: ReturnType<typeof setInterval> | null = null;\nlet _immediateSignal = false;\n\n// ---------------------------------------------------------------------------\n// Worker loop\n// ---------------------------------------------------------------------------\n\n/** Start the background worker and housekeeping timers. */\nexport function startWorker(): void {\n process.stderr.write(\"[work-queue-worker] Starting worker loop.\\n\");\n\n workerTimer = setInterval(async () => {\n try {\n await processNextItem();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Uncaught error in worker loop: ${e}\\n`);\n }\n }, WORKER_INTERVAL_MS);\n\n housekeepingTimer = setInterval(() => {\n try {\n cleanup();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Housekeeping error: ${e}\\n`);\n }\n }, HOUSEKEEPING_INTERVAL_MS);\n\n process.stderr.write(\"[work-queue-worker] Worker started (interval=5s, housekeeping=10min).\\n\");\n}\n\n/** Stop the worker timers gracefully. */\nexport function stopWorker(): void {\n if (workerTimer !== null) {\n clearInterval(workerTimer);\n workerTimer = null;\n }\n if (housekeepingTimer !== null) {\n clearInterval(housekeepingTimer);\n housekeepingTimer = null;\n }\n process.stderr.write(\"[work-queue-worker] Worker stopped.\\n\");\n}\n\n/**\n * Signal that new work has been enqueued.\n * The worker will run on its next tick — we don't need to reset the timer\n * since 5 s is fast enough. The flag allows future optimisations.\n */\nexport function notifyNewWork(): void {\n _immediateSignal = true;\n}\n\n// ---------------------------------------------------------------------------\n// Item processor (sequential — one item per tick)\n// ---------------------------------------------------------------------------\n\nasync function processNextItem(): Promise<void> {\n const item = dequeue();\n if (!item) return;\n\n process.stderr.write(\n `[work-queue-worker] Processing ${item.type} (id=${item.id}, attempt=${item.attempts}).\\n`\n );\n\n try {\n switch (item.type) {\n case \"session-end\":\n await handleSessionEnd(item);\n break;\n\n case \"session-summary\":\n await handleSessionSummary(item.payload as SessionSummaryPayload);\n break;\n\n case \"topic-detect\":\n await handleTopicDetect(item.payload as TopicDetectPayload);\n break;\n\n case \"note-update\":\n case \"todo-update\":\n // Stubs — log and complete\n process.stderr.write(\n `[work-queue-worker] Item type '${item.type}' is not yet implemented — completing as no-op.\\n`\n );\n break;\n\n default:\n throw new Error(`Unknown work item type: ${(item as WorkItem).type}`);\n }\n\n markCompleted(item.id);\n process.stderr.write(`[work-queue-worker] Completed ${item.type} (id=${item.id}).\\n`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n markFailed(item.id, msg);\n }\n}\n\n// ---------------------------------------------------------------------------\n// session-end handler\n// ---------------------------------------------------------------------------\n\n/**\n * Process a 'session-end' work item.\n *\n * Expected payload:\n * transcriptPath: string — absolute path to the .jsonl transcript\n * cwd: string — working directory of the session\n * message?: string — COMPLETED: line extracted by the hook (optional)\n */\nasync function handleSessionEnd(item: WorkItem): Promise<void> {\n const { transcriptPath, cwd, message: hookMessage } = item.payload as {\n transcriptPath: string;\n cwd: string;\n message?: string;\n };\n\n if (!transcriptPath) throw new Error(\"session-end payload missing transcriptPath\");\n if (!cwd) throw new Error(\"session-end payload missing cwd\");\n\n // Read transcript\n let transcript: string;\n try {\n transcript = readFileSync(transcriptPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read transcript at ${transcriptPath}: ${e}`);\n }\n\n const lines = transcript.trim().split(\"\\n\");\n\n // Extract work items from transcript\n const workItems = extractWorkFromTranscript(lines);\n\n // Determine completion message\n let message = hookMessage ?? \"\";\n if (!message) {\n const lastEntry = tryParseJson(lines[lines.length - 1]);\n if (lastEntry?.type === \"assistant\" && lastEntry.message?.content) {\n const content = contentToText(lastEntry.message.content);\n const m = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (m) {\n message = m[1].trim().replace(/\\*+/g, \"\").replace(/\\[.*?\\]/g, \"\").trim();\n }\n }\n }\n\n // Find notes directory and current note\n const notesInfo = findNotesDir(cwd);\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n // Add work items to session note\n if (workItems.length > 0) {\n addWorkToSessionNote(currentNotePath, workItems);\n process.stderr.write(\n `[work-queue-worker] Added ${workItems.length} work item(s) to note.\\n`\n );\n } else if (message) {\n addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);\n process.stderr.write(\"[work-queue-worker] Added completion message to note.\\n\");\n }\n\n // Finalize the note\n const summary = message || \"Session completed.\";\n finalizeSessionNote(currentNotePath, summary);\n process.stderr.write(\n `[work-queue-worker] Finalized session note: ${basename(currentNotePath)}.\\n`\n );\n\n // Update TODO.md ## Continue section\n try {\n const stateLines: string[] = [];\n stateLines.push(`Working directory: ${cwd}`);\n if (workItems.length > 0) {\n stateLines.push(\"\", \"Work completed:\");\n for (const wi of workItems.slice(0, 5)) {\n stateLines.push(`- ${wi.title}`);\n }\n }\n if (message) {\n stateLines.push(\"\", `Last completed: ${message}`);\n }\n updateTodoContinue(cwd, basename(currentNotePath), stateLines.join(\"\\n\"), \"session-end\");\n } catch (todoError) {\n // Non-fatal — log and continue\n process.stderr.write(\n `[work-queue-worker] Could not update TODO.md: ${todoError}\\n`\n );\n }\n } else {\n process.stderr.write(\n \"[work-queue-worker] No current session note found — skipping note update.\\n\"\n );\n }\n\n // Move session .jsonl files to sessions/ subdirectory\n try {\n const transcriptDir = dirname(transcriptPath);\n const movedCount = moveSessionFilesToSessionsDir(transcriptDir);\n if (movedCount > 0) {\n process.stderr.write(\n `[work-queue-worker] Moved ${movedCount} session file(s) to sessions/.\\n`\n );\n }\n } catch (moveError) {\n // Non-fatal\n process.stderr.write(`[work-queue-worker] Could not move session files: ${moveError}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Transcript parsing helpers (mirrors stop-hook.ts logic)\n// ---------------------------------------------------------------------------\n\nfunction tryParseJson(line: string): Record<string, unknown> | null {\n try {\n return JSON.parse(line) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\nfunction extractWorkFromTranscript(lines: string[]): NoteWorkItem[] {\n const workItems: NoteWorkItem[] = [];\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n const entry = tryParseJson(line);\n if (!entry || entry.type !== \"assistant\") continue;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n if (!msg?.content) continue;\n\n const content = contentToText(msg.content);\n\n // SUMMARY: line (preferred)\n const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const summary = summaryMatch[1].trim();\n if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n seenSummaries.add(summary);\n\n const details: string[] = [];\n const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1]\n .split(\"\\n\")\n .map((l) =>\n l.replace(/^[-*•]\\s*/, \"\").replace(/^\\d+\\.\\s*/, \"\").trim()\n )\n .filter((l) => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n\n workItems.push({\n title: summary,\n details: details.length > 0 ? details : undefined,\n completed: true,\n });\n }\n }\n\n // COMPLETED: line (fallback)\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch && workItems.length === 0) {\n const completed = completedMatch[1]\n .trim()\n .replace(/\\*+/g, \"\")\n .replace(/\\[.*?\\]/g, \"\")\n .trim();\n if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n seenSummaries.add(completed);\n workItems.push({ title: completed, completed: true });\n }\n }\n }\n\n return workItems;\n}\n","/**\n * IPC request handler — processes all inbound IPC methods and sends responses.\n */\n\nimport type { Socket } from \"node:net\";\nimport type { IpcRequest, IpcResponse, PostgresBackendWithPool } from \"./types.js\";\nimport type { NotificationMode } from \"../../notifications/types.js\";\nimport {\n patchNotificationConfig,\n} from \"../../notifications/config.js\";\nimport { routeNotification } from \"../../notifications/router.js\";\nimport {\n ensureObservationTables,\n storeObservation,\n queryObservations,\n queryRecentObservations,\n storeSessionSummary,\n} from \"../../observations/store.js\";\nimport {\n registryDb,\n storageBackend,\n daemonConfig,\n startTime,\n indexInProgress,\n lastIndexTime,\n embedInProgress,\n lastEmbedTime,\n vaultIndexInProgress,\n lastVaultIndexTime,\n notificationConfig,\n setNotificationConfig,\n} from \"./state.js\";\nimport { runIndex, runVaultIndex } from \"./scheduler.js\";\nimport { dispatchTool } from \"./dispatcher.js\";\nimport { enqueue, getStats as getQueueStats } from \"../../daemon/work-queue.js\";\nimport { notifyNewWork } from \"../../daemon/work-queue-worker.js\";\n\n// ---------------------------------------------------------------------------\n// Helper\n// ---------------------------------------------------------------------------\n\nexport function sendResponse(socket: Socket, response: IpcResponse): void {\n try {\n socket.write(JSON.stringify(response) + \"\\n\");\n } catch {\n // Socket may already be closed\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single IPC request.\n */\nexport async function handleRequest(\n request: IpcRequest,\n socket: Socket\n): Promise<void> {\n const { id, method, params } = request;\n\n // Special: status\n if (method === \"status\") {\n const dbStats = await (async () => {\n try {\n const fedStats = await storageBackend.getStats();\n const projects = (\n registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM projects\")\n .get() as { n: number }\n ).n;\n return { files: fedStats.files, chunks: fedStats.chunks, projects };\n } catch {\n return null;\n }\n })();\n\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n uptime: Math.floor((Date.now() - startTime) / 1000),\n indexInProgress,\n lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,\n indexIntervalSecs: daemonConfig.indexIntervalSecs,\n embedInProgress,\n lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,\n embedIntervalSecs: daemonConfig.embedIntervalSecs,\n socketPath: daemonConfig.socketPath,\n storageBackend: storageBackend.backendType,\n db: dbStats,\n vaultIndexInProgress,\n lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,\n vaultPath: daemonConfig.vaultPath ?? null,\n workQueue: getQueueStats(),\n },\n });\n socket.end();\n return;\n }\n\n // Special: index_now — trigger immediate index (non-blocking response)\n if (method === \"index_now\") {\n runIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: vault_index_now — trigger immediate vault index (non-blocking response)\n if (method === \"vault_index_now\") {\n runVaultIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: notification_get_config\n if (method === \"notification_get_config\") {\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n config: notificationConfig,\n activeChannels: Object.entries(notificationConfig.channels)\n .filter(([ch, cfg]) => ch !== \"voice\" && (cfg as { enabled: boolean }).enabled)\n .map(([ch]) => ch),\n },\n });\n socket.end();\n return;\n }\n\n // Special: notification_set_config\n if (method === \"notification_set_config\") {\n try {\n const p = params as {\n mode?: NotificationMode;\n channels?: Record<string, unknown>;\n routing?: Record<string, unknown>;\n };\n const updated = patchNotificationConfig({\n mode: p.mode,\n channels: p.channels as Parameters<typeof patchNotificationConfig>[0][\"channels\"],\n routing: p.routing as Parameters<typeof patchNotificationConfig>[0][\"routing\"],\n });\n setNotificationConfig(updated);\n sendResponse(socket, { id, ok: true, result: { config: updated } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // Special: notification_send\n if (method === \"notification_send\") {\n const p = params as {\n event?: string;\n message?: string;\n title?: string;\n };\n\n if (!p.message) {\n sendResponse(socket, { id, ok: false, error: \"notification_send: message is required\" });\n socket.end();\n return;\n }\n\n const event = (p.event as Parameters<typeof routeNotification>[0][\"event\"]) ?? \"info\";\n\n routeNotification(\n { event, message: p.message, title: p.title },\n notificationConfig\n ).then((result) => {\n sendResponse(socket, { id, ok: true, result });\n socket.end();\n }).catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n socket.end();\n });\n return;\n }\n\n // ---- Observation methods (Postgres only) --------------------------------\n\n if (method === \"observation_store\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n type: string;\n title: string;\n narrative?: string;\n tool_name: string;\n tool_input_summary?: string;\n files_read?: string[];\n files_modified?: string[];\n concepts?: string[];\n content_hash?: string;\n cwd?: string;\n };\n\n let project_id: number | null = null;\n let project_slug: string | null = null;\n if (p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n project_id = row.id;\n project_slug = row.slug;\n }\n }\n\n await ensureObservationTables(pool);\n const insertedId = await storeObservation(pool, {\n session_id: p.session_id,\n project_id,\n project_slug,\n type: p.type as \"decision\" | \"bugfix\" | \"feature\" | \"refactor\" | \"discovery\" | \"change\",\n title: p.title,\n narrative: p.narrative ?? null,\n tool_name: p.tool_name,\n tool_input_summary: p.tool_input_summary ?? null,\n files_read: p.files_read ?? [],\n files_modified: p.files_modified ?? [],\n concepts: p.concepts ?? [],\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true, id: insertedId } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_query\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_id?: number;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n const rows = await queryObservations(pool, {\n projectId: p.project_id,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit,\n offset: p.offset,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_recent\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { project_id?: number; cwd?: string; limit?: number };\n const limit = p.limit ?? 20;\n\n let resolvedProjectId = p.project_id;\n let resolvedProjectSlug: string | undefined;\n if (resolvedProjectId === undefined && p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n let rows;\n if (resolvedProjectId !== undefined) {\n rows = await queryRecentObservations(pool, resolvedProjectId, limit);\n } else {\n rows = await queryObservations(pool, { limit });\n }\n sendResponse(socket, { id, ok: true, result: { rows, project_slug: resolvedProjectSlug } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_list — alias for observation_query with project slug resolution\n if (method === \"observation_list\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_slug?: string;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n let projectId: number | undefined;\n if (p.project_slug) {\n const row = registryDb.prepare(\n \"SELECT id FROM projects WHERE slug = ?\"\n ).get(p.project_slug) as { id: number } | undefined;\n projectId = row?.id;\n }\n\n const rows = await queryObservations(pool, {\n projectId,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit ?? 20,\n offset: p.offset ?? 0,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_stats — aggregate statistics\n if (method === \"observation_stats\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n await ensureObservationTables(pool);\n const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([\n pool.query<{ count: string }>(\"SELECT COUNT(*) as count FROM pai_observations\"),\n pool.query<{ type: string; count: string }>(\n \"SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC\"\n ),\n pool.query<{ project_slug: string | null; count: string }>(\n \"SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15\"\n ),\n pool.query<{ created_at: string }>(\n \"SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1\"\n ),\n ]);\n\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n total: parseInt(totalRes.rows[0]?.count ?? \"0\", 10),\n by_type: byTypeRes.rows.map(r => ({ type: r.type, count: parseInt(r.count, 10) })),\n by_project: byProjectRes.rows.map(r => ({ project_slug: r.project_slug, count: parseInt(r.count, 10) })),\n most_recent: recentRes.rows[0]?.created_at ?? null,\n },\n });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"session_summary_store\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Session summaries require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n project_id?: number;\n project_slug?: string;\n cwd?: string;\n request?: string;\n investigated?: string;\n learned?: string;\n completed?: string;\n next_steps?: string;\n observation_count?: number;\n };\n\n let resolvedProjectId = p.project_id ?? null;\n let resolvedProjectSlug = p.project_slug ?? null;\n if (resolvedProjectId === null && p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n await storeSessionSummary(pool, {\n session_id: p.session_id,\n project_id: resolvedProjectId,\n project_slug: resolvedProjectSlug,\n request: p.request ?? null,\n investigated: p.investigated ?? null,\n learned: p.learned ?? null,\n completed: p.completed ?? null,\n next_steps: p.next_steps ?? null,\n observation_count: p.observation_count ?? 0,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_enqueue — hooks push work items here instead of doing heavy work inline\n if (method === \"work_queue_enqueue\") {\n try {\n const p = params as {\n type?: string;\n priority?: number;\n payload?: Record<string, unknown>;\n maxAttempts?: number;\n };\n\n if (!p.type) {\n sendResponse(socket, { id, ok: false, error: \"work_queue_enqueue: type is required\" });\n socket.end();\n return;\n }\n\n const item = enqueue({\n type: p.type as import(\"../../daemon/work-queue.js\").WorkItemType,\n priority: p.priority,\n payload: p.payload ?? {},\n maxAttempts: p.maxAttempts,\n });\n\n notifyNewWork();\n\n sendResponse(socket, { id, ok: true, result: { id: item.id, status: item.status } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_stats — return current queue statistics\n if (method === \"work_queue_stats\") {\n sendResponse(socket, { id, ok: true, result: getQueueStats() });\n socket.end();\n return;\n }\n\n // All other methods: PAI tool dispatch\n try {\n const result = await dispatchTool(method, params);\n sendResponse(socket, { id, ok: true, result });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n}\n","/**\n * IPC server and daemon entry point.\n * Owns: isSocketLive, startIpcServer, serve (exported).\n */\n\nimport { existsSync, unlinkSync } from \"node:fs\";\nimport { createServer, connect, Socket, Server } from \"node:net\";\nimport { setPriority } from \"node:os\";\nimport { openRegistry } from \"../../registry/db.js\";\nimport { createStorageBackend } from \"../../storage/factory.js\";\nimport { configureEmbeddingModel } from \"../../memory/embeddings.js\";\nimport { loadNotificationConfig } from \"../../notifications/config.js\";\nimport type { PaiDaemonConfig } from \"../config.js\";\nimport type { IpcRequest } from \"./types.js\";\nimport {\n setRegistryDb,\n setStorageBackend,\n setDaemonConfig,\n setStartTime,\n setNotificationConfig,\n setShutdownRequested,\n indexInProgress,\n embedInProgress,\n indexSchedulerTimer,\n embedSchedulerTimer,\n storageBackend,\n} from \"./state.js\";\nimport { startIndexScheduler, startEmbedScheduler } from \"./scheduler.js\";\nimport { handleRequest, sendResponse } from \"./handler.js\";\nimport { loadQueue } from \"../../daemon/work-queue.js\";\nimport { startWorker, stopWorker } from \"../../daemon/work-queue-worker.js\";\n\n// ---------------------------------------------------------------------------\n// IPC helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether an existing socket file is actually being served by a live process.\n */\nfunction isSocketLive(path: string): Promise<boolean> {\n return new Promise((resolve) => {\n const client = connect(path);\n const timer = setTimeout(() => { client.destroy(); resolve(false); }, 500);\n client.on(\"connect\", () => { clearTimeout(timer); client.end(); resolve(true); });\n client.on(\"error\", () => { clearTimeout(timer); resolve(false); });\n });\n}\n\n/**\n * Start the Unix Domain Socket IPC server.\n */\nasync function startIpcServer(socketPath: string): Promise<Server> {\n if (existsSync(socketPath)) {\n const live = await isSocketLive(socketPath);\n if (live) {\n throw new Error(\"Another daemon is already running — socket is live. Aborting startup.\");\n }\n try {\n unlinkSync(socketPath);\n process.stderr.write(\"[pai-daemon] Removed stale socket file.\\n\");\n } catch {\n // If we can't remove it, bind will fail with a clear error\n }\n }\n\n const server = createServer((socket: Socket) => {\n let buffer = \"\";\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n let nl: number;\n while ((nl = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n if (line.trim() === \"\") continue;\n\n let request: IpcRequest;\n try {\n request = JSON.parse(line) as IpcRequest;\n } catch {\n sendResponse(socket, { id: \"?\", ok: false, error: \"Invalid JSON\" });\n socket.destroy();\n return;\n }\n\n handleRequest(request, socket).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id: request.id, ok: false, error: msg });\n socket.destroy();\n });\n }\n });\n\n socket.on(\"error\", () => {\n // Client disconnected — nothing to do\n });\n });\n\n server.on(\"error\", (e) => {\n process.stderr.write(`[pai-daemon] IPC server error: ${e}\\n`);\n });\n\n server.listen(socketPath, () => {\n process.stderr.write(\n `[pai-daemon] IPC server listening on ${socketPath}\\n`\n );\n });\n\n return server;\n}\n\n// ---------------------------------------------------------------------------\n// Main daemon entry point\n// ---------------------------------------------------------------------------\n\nexport async function serve(config: PaiDaemonConfig): Promise<void> {\n setDaemonConfig(config);\n setStartTime(Date.now());\n\n setNotificationConfig(loadNotificationConfig());\n\n process.stderr.write(\"[pai-daemon] Starting daemon...\\n\");\n process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\\n`);\n process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\\n`);\n const { notificationConfig } = await import(\"./state.js\");\n process.stderr.write(\n `[pai-daemon] Notification mode: ${notificationConfig.mode}\\n`\n );\n\n // Lower scheduling priority so the daemon yields CPU to interactive sessions\n try { setPriority(process.pid, 10); } catch { /* non-fatal */ }\n\n configureEmbeddingModel(config.embeddingModel);\n\n try {\n setRegistryDb(openRegistry());\n process.stderr.write(\"[pai-daemon] Registry database opened.\\n\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\\n`);\n process.exit(1);\n }\n\n try {\n const backend = await createStorageBackend(config);\n setStorageBackend(backend);\n process.stderr.write(\n `[pai-daemon] Federation backend: ${backend.backendType}\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\\n`);\n process.exit(1);\n }\n\n startIndexScheduler();\n\n if (storageBackend.backendType === \"postgres\") {\n startEmbedScheduler();\n } else {\n process.stderr.write(\n \"[pai-daemon] Embed scheduler: disabled (SQLite backend)\\n\"\n );\n }\n\n // Work queue — load persisted items and start worker loop\n loadQueue();\n startWorker();\n\n const server = await startIpcServer(config.socketPath);\n\n const shutdown = async (signal: string): Promise<void> => {\n process.stderr.write(`\\n[pai-daemon] ${signal} received. Stopping.\\n`);\n\n setShutdownRequested(true);\n\n if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);\n if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);\n\n stopWorker();\n\n server.close();\n\n const SHUTDOWN_TIMEOUT_MS = 10_000;\n const POLL_INTERVAL_MS = 100;\n const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\n `[pai-daemon] Waiting for in-progress operations to finish ` +\n `(index=${indexInProgress}, embed=${embedInProgress})...\\n`\n );\n\n while ((indexInProgress || embedInProgress) && Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Shutdown timeout reached — forcing exit.\\n\");\n } else {\n process.stderr.write(\"[pai-daemon] In-progress operations finished.\\n\");\n }\n }\n\n try {\n await storageBackend.close();\n } catch {\n // ignore\n }\n\n try {\n unlinkSync(config.socketPath);\n } catch {\n // ignore\n }\n\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => { shutdown(\"SIGINT\").catch(() => process.exit(0)); });\n process.on(\"SIGTERM\", () => { shutdown(\"SIGTERM\").catch(() => process.exit(0)); });\n\n // Keep process alive\n await new Promise(() => {});\n}\n","/**\n * Shim — re-exports serve() from daemon/ directory so existing importers\n * continue to work without modification. See daemon/index.ts.\n */\nexport { serve } from \"./daemon/index.js\";\n"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@ async function createStorageBackend(config) {
|
|
|
15
15
|
}
|
|
16
16
|
async function tryPostgres(config) {
|
|
17
17
|
try {
|
|
18
|
-
const { PostgresBackend } = await import("./postgres-
|
|
18
|
+
const { PostgresBackend } = await import("./postgres-DbUXNuy_.mjs");
|
|
19
19
|
const pgConfig = config.postgres ?? {};
|
|
20
20
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
21
21
|
const backend = new PostgresBackend(pgConfig);
|
|
@@ -41,4 +41,4 @@ async function createSQLiteBackend() {
|
|
|
41
41
|
|
|
42
42
|
//#endregion
|
|
43
43
|
export { factory_exports as n, createStorageBackend as t };
|
|
44
|
-
//# sourceMappingURL=factory-
|
|
44
|
+
//# sourceMappingURL=factory-BzWfxsvK.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-
|
|
1
|
+
{"version":3,"file":"factory-BzWfxsvK.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n * If Postgres is configured but unavailable, falls back to SQLite with\n * a warning log — the daemon never crashes due to a missing Postgres.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-fallback behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend if reachable, else SQLiteBackend\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await tryPostgres(config);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\nasync function tryPostgres(config: PaiDaemonConfig): Promise<StorageBackend> {\n try {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n\n // Ensure the per-user database exists and has the schema applied\n await PostgresBackend.ensureDatabase(pgConfig);\n\n const backend = new PostgresBackend(pgConfig);\n\n const err = await backend.testConnection();\n if (err) {\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${err}). Falling back to SQLite.\\n`\n );\n await backend.close();\n return createSQLiteBackend();\n }\n\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n return backend;\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Postgres init error (${msg}). Falling back to SQLite.\\n`\n );\n return createSQLiteBackend();\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;;;;;;;;;;AAkBA,eAAsB,qBACpB,QACyB;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,YAAY,OAAO;AAIlC,QAAO,qBAAqB;;AAG9B,eAAe,YAAY,QAAkD;AAC3E,KAAI;EACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;EACzC,MAAM,WAAW,OAAO,YAAY,EAAE;AAGtC,QAAM,gBAAgB,eAAe,SAAS;EAE9C,MAAM,UAAU,IAAI,gBAAgB,SAAS;EAE7C,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,WAAQ,OAAO,MACb,sCAAsC,IAAI,8BAC3C;AACD,SAAM,QAAQ,OAAO;AACrB,UAAO,qBAAqB;;AAG9B,UAAQ,OAAO,MAAM,kDAAkD;AACvE,SAAO;UACA,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MACb,qCAAqC,IAAI,8BAC1C;AACD,SAAO,qBAAqB;;;AAIhC,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
|