@tekmidian/pai 0.40.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +1 -0
  2. package/dist/aibroker-client-BP74gGwD.mjs +215 -0
  3. package/dist/aibroker-client-BP74gGwD.mjs.map +1 -0
  4. package/dist/aibroker-client-C6m71p6O.mjs +3 -0
  5. package/dist/{auto-route-BwzmlW3W.mjs → auto-route-_5zzJz5q.mjs} +2 -2
  6. package/dist/{auto-route-BwzmlW3W.mjs.map → auto-route-_5zzJz5q.mjs.map} +1 -1
  7. package/dist/browser-mcp/index.d.mts +1 -0
  8. package/dist/browser-mcp/index.mjs +352 -0
  9. package/dist/browser-mcp/index.mjs.map +1 -0
  10. package/dist/{chain-DhVHVnmT.mjs → chain-DGXNoTwc.mjs} +93 -320
  11. package/dist/chain-DGXNoTwc.mjs.map +1 -0
  12. package/dist/cli/index.mjs +12 -10
  13. package/dist/cli/index.mjs.map +1 -1
  14. package/dist/cli/program.mjs +12 -10
  15. package/dist/{clusters-DRHhfli9.mjs → clusters-j0ZuNYp9.mjs} +1 -1
  16. package/dist/{clusters-DRHhfli9.mjs.map → clusters-j0ZuNYp9.mjs.map} +1 -1
  17. package/dist/{config-D1G9IFpn.mjs → config-B64vFg14.mjs} +3 -50
  18. package/dist/config-B64vFg14.mjs.map +1 -0
  19. package/dist/{config-DD52BPB-.mjs → config-C_ErGddD.mjs} +1 -1
  20. package/dist/{config-Dl8lT4Lu.mjs → config-DgzR5sfb.mjs} +2 -2
  21. package/dist/{config-Dl8lT4Lu.mjs.map → config-DgzR5sfb.mjs.map} +1 -1
  22. package/dist/{context-handover-cache-C6_rs7JT.mjs → context-handover-cache-dUnSZwaA.mjs} +4 -4
  23. package/dist/{context-handover-cache-C6_rs7JT.mjs.map → context-handover-cache-dUnSZwaA.mjs.map} +1 -1
  24. package/dist/daemon/index.mjs +13 -12
  25. package/dist/daemon/index.mjs.map +1 -1
  26. package/dist/daemon-CKBkhOsZ.mjs +23 -0
  27. package/dist/{daemon-CpdVUnl_.mjs → daemon-DsvP8KMy.mjs} +357 -27
  28. package/dist/daemon-DsvP8KMy.mjs.map +1 -0
  29. package/dist/daemon-mcp/index.mjs +20 -6
  30. package/dist/daemon-mcp/index.mjs.map +1 -1
  31. package/dist/detector-C-7KRi42.mjs +5 -0
  32. package/dist/{detector-smpCm7ku.mjs → detector-C73Nxm94.mjs} +1 -1
  33. package/dist/{detector-smpCm7ku.mjs.map → detector-C73Nxm94.mjs.map} +1 -1
  34. package/dist/{factory-Bc5EyN54.mjs → factory-DEb9NmHJ.mjs} +7 -7
  35. package/dist/{factory-Bc5EyN54.mjs.map → factory-DEb9NmHJ.mjs.map} +1 -1
  36. package/dist/factory-Dbo8yNPi.mjs +3 -0
  37. package/dist/{fallback-CdXv-Np0.mjs → fallback-5UJO-RyX.mjs} +5 -4
  38. package/dist/{fallback-CdXv-Np0.mjs.map → fallback-5UJO-RyX.mjs.map} +1 -1
  39. package/dist/hooks/worker-status-line.mjs +9 -4
  40. package/dist/hooks/worker-status-line.mjs.map +3 -3
  41. package/dist/hooks/worker-supervision.mjs +593 -0
  42. package/dist/hooks/worker-supervision.mjs.map +7 -0
  43. package/dist/{indexer-backend-rohVVKrA.mjs → indexer-backend-BPTzKFZU.mjs} +1 -1
  44. package/dist/{indexer-backend-rohVVKrA.mjs.map → indexer-backend-BPTzKFZU.mjs.map} +1 -1
  45. package/dist/{ipc-client-C7AVpsyv.mjs → ipc-client-BmypMNYk.mjs} +2 -2
  46. package/dist/{ipc-client-C7AVpsyv.mjs.map → ipc-client-BmypMNYk.mjs.map} +1 -1
  47. package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
  48. package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-iNp5s55R.mjs → latent-ideas-CtSca4qG.mjs} +2 -2
  50. package/dist/{latent-ideas-iNp5s55R.mjs.map → latent-ideas-CtSca4qG.mjs.map} +1 -1
  51. package/dist/{link-boost-NNzADHX_.mjs → link-boost-Bucnz3n8.mjs} +1 -1
  52. package/dist/{link-boost-NNzADHX_.mjs.map → link-boost-Bucnz3n8.mjs.map} +1 -1
  53. package/dist/{main-resolver-CM1IHbuu.mjs → main-resolver-BgIrC-D9.mjs} +4 -214
  54. package/dist/main-resolver-BgIrC-D9.mjs.map +1 -0
  55. package/dist/main-resolver-DXGuygFG.mjs +7 -0
  56. package/dist/{neighborhood-AGL7oXQ2.mjs → neighborhood-CM7Jgb51.mjs} +1 -1
  57. package/dist/{neighborhood-AGL7oXQ2.mjs.map → neighborhood-CM7Jgb51.mjs.map} +1 -1
  58. package/dist/{note-context-BHwYpSFf.mjs → note-context-DCwE2A-M.mjs} +1 -1
  59. package/dist/{note-context-BHwYpSFf.mjs.map → note-context-DCwE2A-M.mjs.map} +1 -1
  60. package/dist/{planner-Cm3g6fWH.mjs → planner-BLDJt3ZS.mjs} +6 -5
  61. package/dist/{planner-Cm3g6fWH.mjs.map → planner-BLDJt3ZS.mjs.map} +1 -1
  62. package/dist/{postgres-Dwxe290H.mjs → postgres-CtOzfoQU.mjs} +1 -1
  63. package/dist/{postgres-Dwxe290H.mjs.map → postgres-CtOzfoQU.mjs.map} +1 -1
  64. package/dist/{program-AR0qlYRg.mjs → program-CruRnLtc.mjs} +99 -84
  65. package/dist/program-CruRnLtc.mjs.map +1 -0
  66. package/dist/query-feedback-DGoVRaWD.mjs +3 -0
  67. package/dist/{query-feedback-DZfjI34w.mjs → query-feedback-Dqz-HC__.mjs} +1 -1
  68. package/dist/{query-feedback-DZfjI34w.mjs.map → query-feedback-Dqz-HC__.mjs.map} +1 -1
  69. package/dist/router-BTIGZR2_.mjs +3 -0
  70. package/dist/{router-DHracTUC.mjs → router-DD6D4j31.mjs} +1 -1
  71. package/dist/{router-DHracTUC.mjs.map → router-DD6D4j31.mjs.map} +1 -1
  72. package/dist/runtime-paths-rni52zHX.mjs +50 -0
  73. package/dist/runtime-paths-rni52zHX.mjs.map +1 -0
  74. package/dist/scope-NIwCGxRI.mjs +351 -0
  75. package/dist/scope-NIwCGxRI.mjs.map +1 -0
  76. package/dist/{server-BOuAOj9b.mjs → server-Cp7tNzNC.mjs} +19 -2
  77. package/dist/server-Cp7tNzNC.mjs.map +1 -0
  78. package/dist/skills/Worker/SKILL.md +13 -0
  79. package/dist/{sources-ju6AA7lL.mjs → sources-CS43CpuS.mjs} +1 -1
  80. package/dist/{sources-ju6AA7lL.mjs.map → sources-CS43CpuS.mjs.map} +1 -1
  81. package/dist/{sqlite-kFZeWCJ2.mjs → sqlite-Cq3gJcq9.mjs} +1 -1
  82. package/dist/{sqlite-kFZeWCJ2.mjs.map → sqlite-Cq3gJcq9.mjs.map} +1 -1
  83. package/dist/{state-ahFAbD8b.mjs → state-BTa10SOM.mjs} +1 -1
  84. package/dist/{state-ahFAbD8b.mjs.map → state-BTa10SOM.mjs.map} +1 -1
  85. package/dist/{state-DH_mp5Xe.mjs → state-RXepvbnO.mjs} +1 -1
  86. package/dist/{themes-DqhNUUQk.mjs → themes-B7h9YDiy.mjs} +1 -1
  87. package/dist/{themes-DqhNUUQk.mjs.map → themes-B7h9YDiy.mjs.map} +1 -1
  88. package/dist/{tools-COna9vZR.mjs → tools-BW3NLMPk.mjs} +1 -1
  89. package/dist/{tools-CMlHa9lZ.mjs → tools-LlzcV_eK.mjs} +14 -14
  90. package/dist/{tools-CMlHa9lZ.mjs.map → tools-LlzcV_eK.mjs.map} +1 -1
  91. package/dist/{trace-DTP6k4B_.mjs → trace-u4hT-pLD.mjs} +1 -1
  92. package/dist/{trace-DTP6k4B_.mjs.map → trace-u4hT-pLD.mjs.map} +1 -1
  93. package/dist/{vault-indexer-YMH22mLU.mjs → vault-indexer-mata9S7b.mjs} +1 -1
  94. package/dist/{vault-indexer-YMH22mLU.mjs.map → vault-indexer-mata9S7b.mjs.map} +1 -1
  95. package/dist/{work-queue-worker-B3nl8yyn.mjs → work-queue-worker-7Vkli4Yh.mjs} +4 -4
  96. package/dist/{work-queue-worker-B3nl8yyn.mjs.map → work-queue-worker-7Vkli4Yh.mjs.map} +1 -1
  97. package/dist/work-queue-worker-D1p2V-ae.mjs +14 -0
  98. package/dist/{zettelkasten-DDvKRm2V.mjs → zettelkasten-F0D_XfQ4.mjs} +3 -3
  99. package/dist/{zettelkasten-DDvKRm2V.mjs.map → zettelkasten-F0D_XfQ4.mjs.map} +1 -1
  100. package/docs/commands/README.md +2 -2
  101. package/docs/commands/mcp.md +4 -4
  102. package/docs/commands/worker.md +3 -0
  103. package/docs/worker.md +40 -0
  104. package/package.json +1 -1
  105. package/plugins/productivity/hooks/hooks.json +5 -0
  106. package/src/hooks/ts/user-prompt/worker-supervision.ts +123 -0
  107. package/statusline-command.sh +65 -73
  108. package/dist/chain-DhVHVnmT.mjs.map +0 -1
  109. package/dist/config-D1G9IFpn.mjs.map +0 -1
  110. package/dist/daemon-BSL9sXxm.mjs +0 -22
  111. package/dist/daemon-CpdVUnl_.mjs.map +0 -1
  112. package/dist/detector-BLVOCiJ_.mjs +0 -5
  113. package/dist/factory-C7ua_3ik.mjs +0 -3
  114. package/dist/main-resolver-CM1IHbuu.mjs.map +0 -1
  115. package/dist/main-resolver-E5WlilhM.mjs +0 -6
  116. package/dist/program-AR0qlYRg.mjs.map +0 -1
  117. package/dist/query-feedback-CGcEiGtY.mjs +0 -3
  118. package/dist/router-CAtyVQud.mjs +0 -3
  119. package/dist/server-BOuAOj9b.mjs.map +0 -1
  120. package/dist/work-queue-worker-BM1GcAAq.mjs +0 -14
@@ -1 +0,0 @@
1
- {"version":3,"file":"daemon-CpdVUnl_.mjs","names":["getQueueStats"],"sources":["../src/notifications/config.ts","../src/daemon/daemon/scheduler.ts","../src/observations/store.ts","../src/daemon/daemon/dispatcher.ts","../src/daemon/daemon/handler.ts","../src/daemon/daemon/server.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, vault index, and registry scan schedulers for the PAI daemon.\n * Exports run* functions (also called on-demand by the IPC handler)\n * and the start* functions invoked once at daemon startup.\n */\n\nimport { 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 // Indexing and embedding are mutually exclusive (each defers to the other),\n // and an index pass regularly outlasts the index interval. Left to two\n // independent timers, the embed tick therefore lands while indexing is in\n // progress and is dropped — observed live on 2026-08-01 as six consecutive\n // index passes with no embed pass at all, and a 143k-chunk embedding backlog.\n // Chaining embed onto the end of every index pass makes the alternation\n // guaranteed rather than a race: index, then vault, then embed, every cycle.\n const cycle = (label: string, withEmbed: boolean) =>\n runIndex()\n .then(() => runVaultIndex())\n .then(() => (withEmbed ? runEmbed() : undefined))\n .catch((e) => {\n process.stderr.write(`[pai-daemon] ${label} index error: ${e}\\n`);\n });\n\n // The startup pass indexes but must NOT embed unless embedOnStartup says so.\n // startEmbedScheduler already refuses a startup embed for exactly that reason;\n // chaining runEmbed onto this pass reinstated the same CPU storm through the\n // back door. Seen live after a reboot: \"Startup embed pass skipped\n // (embedOnStartup=false)\" in the log, and minutes later the same boot running\n // a 5000-chunk pass at ~450% CPU. A guard one scheduler honours and another\n // walks around is not a guard.\n // With a maintenance hour configured there is no startup pass at all. Every\n // restart otherwise buys itself a full index run, so on a machine that is\n // rebooted or has the daemon reloaded during the day the \"maintenance runs at\n // night\" guarantee is worth nothing: the heavy work simply follows the\n // restarts around. Indexing waits for the window like everything else.\n if (daemonConfig.maintenanceHour === undefined) {\n setTimeout(() => void cycle(\"Startup\", daemonConfig.embedOnStartup), 2_000);\n } else {\n process.stderr.write(\n \"[pai-daemon] Startup index pass skipped (maintenanceHour is set).\\n\"\n );\n }\n\n // Anchor the recurring cycle to a wall-clock hour when one is configured.\n // setInterval alone counts from daemon start, so a machine rebooted at noon\n // gets its \"daily\" maintenance at noon every day thereafter — the interval\n // says how often, never when. Without an anchor there is no such thing as a\n // night-only pass, however long the interval.\n const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);\n if (daemonConfig.maintenanceHour !== undefined) {\n process.stderr.write(\n `[pai-daemon] Index scheduler anchored to ${String(daemonConfig.maintenanceHour).padStart(2, \"0\")}:00 local ` +\n `(next pass in ${Math.round(firstDelayMs / 60_000)} min)\\n`\n );\n }\n\n let timer: ReturnType<typeof setInterval>;\n const startTimer = () => {\n timer = setInterval(() => void cycle(\"Scheduled\", true), intervalMs);\n if (timer.unref) timer.unref();\n setIndexSchedulerTimer(timer);\n };\n\n const first = setTimeout(() => {\n void cycle(\"Scheduled\", true);\n startTimer();\n }, firstDelayMs);\n if (first.unref) first.unref();\n}\n\n/**\n * Milliseconds until the next occurrence of `hour`:00 local time.\n * Falls back to the plain interval when no anchor hour is configured.\n */\nfunction msUntilNextAnchor(hour: number | undefined, intervalMs: number): number {\n if (hour === undefined || !Number.isInteger(hour) || hour < 0 || hour > 23) {\n return intervalMs;\n }\n const now = new Date();\n const next = new Date(now);\n next.setHours(hour, 0, 0, 0);\n if (next <= now) next.setDate(next.getDate() + 1);\n return next.getTime() - now.getTime();\n}\n\n// ---------------------------------------------------------------------------\n// Embed scheduler\n// ---------------------------------------------------------------------------\n\n/**\n * Run an embedding pass for all unembedded chunks (Postgres backend only).\n */\nexport async function runEmbed(): Promise<void> {\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index in progress, deferring embed pass.\\n\");\n return;\n }\n\n if (storageBackend.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 // Explicit budgets so one embed phase can never outlast an index cycle.\n // Backlogs drain across passes, not within one.\n const count = await embedChunksWithBackend(\n storageBackend,\n () => shutdownRequested,\n projectNames,\n // Indexing settles to a few chunks per pass once caught up, so the daemon\n // is idle most of the cycle and the embed budget is what actually sets\n // drain rate. Measured 450 chunks per 90s against real (long) chunks, so\n // a backlog only clears at roughly the budget's share of the cycle. What\n // broke the starvation was bounding the pass at all, not this number —\n // it can be raised freely as long as it stays finite.\n { maxMillis: 240_000 },\n );\n\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 { maxMillis: 45_000 },\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 // Startup embed pass — OFF unless explicitly enabled.\n //\n // This used to run unconditionally 60s after every start, ignoring\n // embedIntervalSecs entirely. With a large backlog that makes every restart a\n // CPU storm: the interval was set to a day to stop the machine being bogged\n // down, the daemon was restarted, and it immediately began a 240-second pass\n // anyway. Throttling the recurring timer while leaving this armed means the\n // throttle silently does nothing for anyone who restarts.\n //\n // Measured backlog when this was found: 1,868,098 unembedded chunks at ~5.7\n // chunks/s — about 91 days of work that a restart would resume unbidden.\n if (daemonConfig.embedOnStartup) {\n setTimeout(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Startup embed error: ${e}\\n`);\n });\n }, 60_000);\n } else {\n process.stderr.write(\n \"[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\\n\"\n );\n }\n\n // With a maintenance hour configured the index cycle already chains an embed\n // pass onto every anchored run, so this timer would only add a second pass\n // that counts from daemon start — observed as \"every 86400s\" armed at a\n // 20:08 boot, i.e. a daily daytime pass on a machine configured for 03:00.\n // The interval says how often, never when; without an anchor it cannot\n // deliver a night-only pass, so it is not armed at all.\n if (daemonConfig.maintenanceHour !== undefined) {\n process.stderr.write(\n \"[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; \" +\n \"embed runs at the end of each anchored index cycle).\\n\"\n );\n return;\n }\n\n const timer = setInterval(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setEmbedSchedulerTimer(timer);\n}\n\n// ---------------------------------------------------------------------------\n// Registry scan scheduler\n// ---------------------------------------------------------------------------\n\nconst REGISTRY_SCAN_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes\nconst REGISTRY_SCAN_STARTUP_DELAY_MS = 10_000; // 10 seconds after daemon start\n\n/**\n * Start the periodic registry scan scheduler.\n * First scan runs 10 seconds after startup (enough time for IPC to stabilise).\n * Subsequent scans run every 30 minutes.\n * Uses the work-queue so the scan runs on the worker thread without blocking IPC.\n */\nexport function startRegistryScanScheduler(): void {\n process.stderr.write(\n \"[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\\n\"\n );\n\n setTimeout(() => {\n import(\"../../daemon/work-queue-worker.js\")\n .then(({ enqueueRegistryScan }) => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_STARTUP_DELAY_MS);\n\n const timer = setInterval(() => {\n import(\"../../daemon/work-queue-worker.js\")\n .then(({ enqueueRegistryScan }) => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_INTERVAL_MS);\n\n if (timer.unref) timer.unref();\n}\n","/**\n * 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// Skill telemetry — self-educating skill system, Phase 1 (capture)\n// ---------------------------------------------------------------------------\n\nexport interface SkillTelemetryRow {\n id: number;\n scope: string;\n skill_name: string;\n source: string;\n status: string;\n trigger_count: number;\n accept_count: number;\n first_triggered: Date;\n last_triggered: Date;\n context_projects: string[];\n hash: string | null;\n audit_status: string | null;\n last_audited: Date | null;\n}\n\nexport interface RecordSkillInvocationInput {\n skill_name: string;\n /** 'local' | 'skills.sh' | repo slug */\n source?: string;\n /** governance seam — defaults to 'default' */\n scope?: string;\n /** project slug for context_projects rollup */\n project_slug?: string | null;\n}\n\nexport interface QuerySkillTelemetryOptions {\n scope?: string;\n status?: string;\n limit?: number;\n}\n\nconst SKILL_TELEMETRY_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS pai_skill_telemetry (\n id SERIAL PRIMARY KEY,\n scope TEXT NOT NULL DEFAULT 'default',\n skill_name TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'local',\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('trial','active','archived')),\n trigger_count INTEGER NOT NULL DEFAULT 0,\n accept_count INTEGER NOT NULL DEFAULT 0,\n first_triggered TIMESTAMPTZ DEFAULT NOW(),\n last_triggered TIMESTAMPTZ DEFAULT NOW(),\n context_projects JSONB DEFAULT '[]'::jsonb,\n hash TEXT,\n audit_status TEXT,\n last_audited TIMESTAMPTZ,\n UNIQUE(scope, skill_name, source)\n);\n\nCREATE INDEX IF NOT EXISTS idx_skilltel_scope ON pai_skill_telemetry(scope);\nCREATE INDEX IF NOT EXISTS idx_skilltel_status ON pai_skill_telemetry(scope, status);\nCREATE INDEX IF NOT EXISTS idx_skilltel_last ON pai_skill_telemetry(last_triggered DESC);\n`;\n\nlet _skillTelemetryEnsured = false;\n\nexport async function ensureSkillTelemetryTable(pool: Pool): Promise<void> {\n if (_skillTelemetryEnsured) return;\n await pool.query(SKILL_TELEMETRY_SCHEMA_SQL);\n _skillTelemetryEnsured = true;\n}\n\n/**\n * Record a single skill invocation. Upserts on (scope, skill_name, source):\n * increments trigger_count, refreshes last_triggered, and dedup-merges the\n * project slug into context_projects.\n */\nexport async function recordSkillInvocation(\n pool: Pool,\n input: RecordSkillInvocationInput\n): Promise<void> {\n await ensureSkillTelemetryTable(pool);\n\n const scope = input.scope ?? 'default';\n const source = input.source ?? 'local';\n const proj = input.project_slug ?? null;\n const initialProjects = JSON.stringify(proj ? [proj] : []);\n\n await pool.query(\n `INSERT INTO pai_skill_telemetry\n (scope, skill_name, source, trigger_count, context_projects)\n VALUES ($1, $2, $3, 1, $4::jsonb)\n ON CONFLICT (scope, skill_name, source) DO UPDATE SET\n trigger_count = pai_skill_telemetry.trigger_count + 1,\n last_triggered = NOW(),\n context_projects = CASE\n WHEN $5::text IS NULL THEN pai_skill_telemetry.context_projects\n WHEN pai_skill_telemetry.context_projects @> to_jsonb($5::text)\n THEN pai_skill_telemetry.context_projects\n ELSE pai_skill_telemetry.context_projects || to_jsonb($5::text)\n END`,\n [scope, input.skill_name, source, initialProjects, proj]\n );\n}\n\n/**\n * List skill telemetry rows, most-triggered first.\n */\nexport async function querySkillTelemetry(\n pool: Pool,\n opts: QuerySkillTelemetryOptions = {}\n): Promise<SkillTelemetryRow[]> {\n await ensureSkillTelemetryTable(pool);\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n let idx = 1;\n\n conditions.push(`scope = $${idx++}`);\n params.push(opts.scope ?? 'default');\n\n if (opts.status !== undefined) {\n conditions.push(`status = $${idx++}`);\n params.push(opts.status);\n }\n\n const where = `WHERE ${conditions.join(' AND ')}`;\n const limit = opts.limit ?? 100;\n params.push(limit);\n\n const result = await pool.query<SkillTelemetryRow>(\n `SELECT id, scope, skill_name, source, status,\n trigger_count, accept_count,\n first_triggered, last_triggered,\n context_projects, hash, audit_status, last_audited\n FROM pai_skill_telemetry\n ${where}\n ORDER BY trigger_count DESC, last_triggered DESC\n LIMIT $${idx}`,\n params\n );\n\n return result.rows;\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 toolMemoryWakeup,\n toolMemoryTaxonomy,\n toolMemoryFeedback,\n toolMemoryKgSearch,\n} from \"../../mcp/tools.js\";\nimport { detectTopicShift } from \"../../topics/detector.js\";\nimport { registryDb, storageBackend, daemonConfig } from \"./state.js\";\nimport { openFederation } from \"../../memory/db.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 \"memory_wakeup\":\n return toolMemoryWakeup(registryDb, p as Parameters<typeof toolMemoryWakeup>[1]);\n\n case \"memory_taxonomy\":\n return toolMemoryTaxonomy(registryDb, storageBackend, p as Parameters<typeof toolMemoryTaxonomy>[2]);\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 case \"kg_add\":\n case \"kg_query\":\n case \"kg_invalidate\":\n case \"kg_contradictions\": {\n const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import(\"../../mcp/tools.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n if (!pgPool) {\n throw new Error(`${method} requires a Postgres storage backend`);\n }\n switch (method) {\n case \"kg_add\": return toolKgAdd(pgPool, p as Parameters<typeof toolKgAdd>[1]);\n case \"kg_query\": return toolKgQuery(pgPool, p as Parameters<typeof toolKgQuery>[1]);\n case \"kg_invalidate\": return toolKgInvalidate(pgPool, p as Parameters<typeof toolKgInvalidate>[1]);\n case \"kg_contradictions\": return toolKgContradictions(pgPool, p as Parameters<typeof toolKgContradictions>[1]);\n }\n break;\n }\n\n case \"memory_tunnels\": {\n const { toolMemoryTunnels } = await import(\"../../mcp/tools.js\");\n return toolMemoryTunnels(registryDb, storageBackend, p as Parameters<typeof toolMemoryTunnels>[2]);\n }\n\n case \"memory_feedback\": {\n // MR2: feedback weight loop — uses federation.db (SQLite) directly\n const federationDb = openFederation();\n try {\n return await toolMemoryFeedback(federationDb, p as Parameters<typeof toolMemoryFeedback>[1]);\n } finally {\n federationDb.close();\n }\n }\n\n case \"memory_kg_search\": {\n // MR1: graph-completion retrieval — federation.db (SQLite) + Postgres pool\n const federationDb = openFederation();\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n if (!pgPool) {\n federationDb.close();\n throw new Error(\"memory_kg_search requires a Postgres storage backend for KG triple expansion\");\n }\n try {\n return await toolMemoryKgSearch(federationDb, pgPool, p as Parameters<typeof toolMemoryKgSearch>[2]);\n } finally {\n federationDb.close();\n }\n }\n\n default:\n throw new Error(`Unknown method: ${method}`);\n }\n}\n","/**\n * IPC request handler — processes all inbound IPC methods and sends responses.\n */\n\nimport type { Socket } from \"node:net\";\nimport type { IpcRequest, IpcResponse, 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 recordSkillInvocation,\n querySkillTelemetry,\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\";\nimport { getBackendOutage } from \"../../storage/outage.js\";\n\n// ---------------------------------------------------------------------------\n// Helper\n// ---------------------------------------------------------------------------\n\nexport function sendResponse(socket: Socket, response: IpcResponse): void {\n try {\n socket.write(JSON.stringify(response) + \"\\n\");\n } catch {\n // Socket may already be closed\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single IPC request.\n */\nexport async function handleRequest(\n request: IpcRequest,\n socket: Socket\n): Promise<void> {\n const { id, method, params } = request;\n\n // Special: status\n if (method === \"status\") {\n const dbStats = await (async () => {\n try {\n if (!storageBackend) return null;\n const fedStats = await storageBackend.getStats();\n const projects = (\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 ?? \"initializing\",\n db: dbStats,\n vaultIndexInProgress,\n lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,\n vaultPath: daemonConfig.vaultPath ?? null,\n workQueue: getQueueStats(),\n // Null when the backend is answering. Present, this is the reason\n // everything else in this payload is stalled — and the reason status\n // used to report \"idle\" for two days while nothing progressed.\n backendOutage: getBackendOutage(),\n },\n });\n socket.end();\n return;\n }\n\n // Special: index_now — trigger immediate index (non-blocking response)\n if (method === \"index_now\") {\n runIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: vault_index_now — trigger immediate vault index (non-blocking response)\n if (method === \"vault_index_now\") {\n runVaultIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: notification_get_config\n if (method === \"notification_get_config\") {\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n config: notificationConfig,\n activeChannels: Object.entries(notificationConfig.channels)\n .filter(([ch, cfg]) => ch !== \"voice\" && (cfg as { enabled: boolean }).enabled)\n .map(([ch]) => ch),\n },\n });\n socket.end();\n return;\n }\n\n // Special: notification_set_config\n if (method === \"notification_set_config\") {\n try {\n const p = params as {\n mode?: NotificationMode;\n channels?: Record<string, unknown>;\n routing?: Record<string, unknown>;\n };\n const updated = patchNotificationConfig({\n mode: p.mode,\n channels: p.channels as Parameters<typeof patchNotificationConfig>[0][\"channels\"],\n routing: p.routing as Parameters<typeof patchNotificationConfig>[0][\"routing\"],\n });\n setNotificationConfig(updated);\n sendResponse(socket, { id, ok: true, result: { config: updated } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // Special: notification_send\n if (method === \"notification_send\") {\n const p = params as {\n event?: string;\n message?: string;\n title?: string;\n };\n\n if (!p.message) {\n sendResponse(socket, { id, ok: false, error: \"notification_send: message is required\" });\n socket.end();\n return;\n }\n\n const event = (p.event as Parameters<typeof routeNotification>[0][\"event\"]) ?? \"info\";\n\n routeNotification(\n { event, message: p.message, title: p.title },\n notificationConfig\n ).then((result) => {\n sendResponse(socket, { id, ok: true, result });\n socket.end();\n }).catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n socket.end();\n });\n return;\n }\n\n // ---- Observation methods (Postgres only) --------------------------------\n\n if (method === \"observation_store\") {\n 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 // ---- Skill telemetry (Postgres only) ------------------------------------\n\n if (method === \"skill_telemetry_record\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n skill_name: string;\n source?: string;\n scope?: string;\n cwd?: string;\n };\n\n let project_slug: string | null = null;\n if (p.cwd) {\n const row = registryDb.prepare(\n \"SELECT slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { slug: string } | undefined;\n if (row) project_slug = row.slug;\n }\n\n await recordSkillInvocation(pool, {\n skill_name: p.skill_name,\n source: p.source,\n scope: p.scope,\n project_slug,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"skill_telemetry_query\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { scope?: string; status?: string; limit?: number };\n const rows = await querySkillTelemetry(pool, {\n scope: p.scope,\n status: p.status,\n limit: p.limit,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_query\") {\n 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, startRegistryScanScheduler } 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 // Start the IPC server immediately so `pai daemon status` answers even while\n // we are still waiting for the federation backend (e.g. Postgres coming up\n // after a reboot). The status handler tolerates a not-yet-ready backend.\n const server = await startIpcServer(config.socketPath);\n\n // Registry scan scheduler only needs the registry DB — safe to start now.\n startRegistryScanScheduler();\n\n // Connect the federation backend in the background. When storageBackend is\n // \"postgres\" this retries forever (never silently falls back to SQLite), so\n // the boot race with Docker/Postgres resolves itself once Postgres is up.\n // Only then do we start the federation-dependent schedulers and work queue —\n // this also keeps the first index run from firing against an unready backend.\n void createStorageBackend(config, { waitForPostgres: true })\n .then((backend) => {\n setStorageBackend(backend);\n process.stderr.write(\n `[pai-daemon] Federation backend: ${backend.backendType}\\n`\n );\n\n startIndexScheduler();\n\n if (backend.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 .catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Fatal: Could not open federation storage: ${msg}\\n`\n );\n process.exit(1);\n });\n\n 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"],"mappings":"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"}
@@ -1,5 +0,0 @@
1
- import "./embeddings-DOLZnT1X.mjs";
2
- import "./search-Rpk1cSBC.mjs";
3
- import { t as detectTopicShift } from "./detector-smpCm7ku.mjs";
4
-
5
- export { detectTopicShift };
@@ -1,3 +0,0 @@
1
- import { t as createStorageBackend } from "./factory-Bc5EyN54.mjs";
2
-
3
- export { createStorageBackend };
@@ -1 +0,0 @@
1
- {"version":3,"file":"main-resolver-CM1IHbuu.mjs","names":["shortenProject"],"sources":["../src/cli/lib/session-scan.ts","../src/cli/lib/aibroker-client.ts","../src/cli/lib/exit-dir.ts","../src/cli/lib/launch.ts","../src/cli/lib/history-search.ts","../src/cli/lib/dedup-sessions.ts","../src/cli/commands/main-resolver.ts"],"sourcesContent":["/**\n * Shared scanner for Claude Code sessions stored in ~/.claude/projects/ .\n *\n * Storage layout (Claude Code v2.1.143+):\n *\n * <project>/<uuid>.jsonl — top-level: metadata, system snapshots, hook events.\n * REQUIRED for claude --resume to work.\n * <project>/sessions/<uuid>.jsonl — full transcript: user/assistant/attachment lines.\n * 3000+ files, almost none resumable on their own.\n *\n * Resumability rule (empirically verified):\n * A session is resumable iff the TOP-LEVEL jsonl exists AND contains at least one\n * line of type \"system\". Sessions that only have a sessions/ counterpart cannot be\n * resumed by Claude Code regardless of how much transcript content they have.\n *\n * Stale-UUID problem (fixed in this version):\n * The clc session.json registry stores ONE uuid per named session — the uuid Claude Code\n * had when the user last named the session. If the user resumes and the session gets a\n * new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD\n * uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project\n * directory, not the clc-cached uuid.\n *\n * Resolution strategy:\n * 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).\n * 2. For each, attach the clc name if the uuid matches (exact hit).\n * 3. After Pass 1, for every clc registry entry:\n * a. If the cached uuid was found → already handled.\n * b. Find the encodedDir for this entry's directory.\n * c. Check ALL sessions in that encodedDir (already in our Pass-1 results).\n * d. Pick the MOST RECENT resumable session in that dir and attach the name to it.\n * If no resumable session, fall through to transcript-only pass.\n * 4. This means the displayed uuid for \"Jobs Alpha\" is always today's active session,\n * not the stale cached one.\n *\n * Used by: pai sessions, pai resume\n */\n\nimport { readdirSync, statSync, readFileSync, existsSync, realpathSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport { smartDecodeDir } from \"../utils.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Display status for a session:\n * resumable — top-level jsonl exists with ≥1 system line (claude --resume works)\n * stub — in clc registry, top-level jsonl exists but 0 system lines (Ctrl+C exit)\n * transcript-only — in clc registry but no top-level jsonl at all\n * orphan — not in clc registry, only sessions/ transcript exists\n */\nexport type SessionStatus = \"resumable\" | \"stub\" | \"transcript-only\" | \"orphan\";\n\nexport interface ScannedSession {\n /** Full UUID from the filename. */\n uuid: string;\n /** Short 8-char prefix for display. */\n shortId: string;\n /** Encoded project directory name (e.g. \"-Users-foo-myproject\"). */\n encodedDir: string;\n /** Decoded project root path (via smartDecodeDir). */\n decodedPath: string;\n\n // Top-level jsonl — the resumability source\n /** Full path to the top-level <project>/<uuid>.jsonl (may not exist). */\n topLevelPath: string;\n /** Number of \"system\" lines in the top-level jsonl. */\n topLevelSystemLines: number;\n /** Size of the top-level jsonl in bytes. */\n topLevelSize: number;\n /**\n * True iff the top-level jsonl has at least one \"system\" line.\n * This is the canonical resumability gate — matches Claude Code's --resume behaviour.\n */\n resumable: boolean;\n /** Categorised display status. */\n sessionStatus: SessionStatus;\n\n // Sessions/ transcript data (best-effort, may not exist)\n /** Full path to sessions/<uuid>.jsonl, if present. */\n sessionJsonlPath?: string;\n /** Number of \"user\" type lines in the transcript. */\n userLines: number;\n /** Last user message text (truncated to 80 chars). */\n lastUserPrompt: string;\n /** Total line count in the transcript file. */\n msgCount: number;\n /** Auto-generated title from the last ai-title line in the transcript. */\n aiTitle?: string;\n\n /** mtime in epoch ms — top-level file mtime (primary), transcript mtime (fallback). */\n mtime: number;\n\n /**\n * Human-readable friendly name (for display and /Name restoration).\n * Priority: clc session.json name → ai-title → project dir basename.\n * NEVER slugified — preserves original spaces and capitalisation.\n */\n friendlyName?: string;\n /**\n * Literal directory from the clc session registry for this UUID.\n * May contain symlinks and emoji (e.g. /Users/foo/dev/... or .../🧠 Vault/...).\n * When present, realpathSync(clcDirectory) gives the correct cwd for --resume.\n */\n clcDirectory?: string;\n /** Project root path from PAI registry (authoritative cwd for resume). */\n registryRootPath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// clc session registry (~/.claude/session.json)\n// ---------------------------------------------------------------------------\n\nconst CLC_SESSIONS_FILE = join(homedir(), \".claude\", \"session.json\");\n\ninterface ClcEntry {\n name?: string;\n resume?: string;\n session?: string;\n directory?: string;\n}\n\ninterface ClcRegistry {\n sessions?: ClcEntry[];\n}\n\nexport interface ClcInfo {\n name: string;\n /** Literal directory from clc registry (may contain symlinks, emoji, spaces). */\n directory?: string;\n}\n\n/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */\nfunction buildClcInfoMap(): Map<string, ClcInfo> {\n const map = new Map<string, ClcInfo>();\n try {\n const raw = readFileSync(CLC_SESSIONS_FILE, \"utf8\");\n const data = JSON.parse(raw) as ClcRegistry;\n for (const entry of data.sessions ?? []) {\n const name = entry.name?.trim();\n const uuid = (entry.resume ?? entry.session ?? \"\").trim();\n if (name && uuid) {\n map.set(uuid, { name, directory: entry.directory?.trim() || undefined });\n }\n }\n } catch {\n // Missing or malformed — not fatal\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// PAI registry\n// ---------------------------------------------------------------------------\n\ninterface RegistryProject {\n root_path: string;\n encoded_dir: string;\n}\n\n/** Build encoded_dir → root_path map from PAI registry (authoritative cwd). */\nfunction buildRegistryRootPathMap(db: Database): Map<string, string> {\n try {\n const rows = db\n .prepare(\n `SELECT root_path, encoded_dir FROM projects\n WHERE encoded_dir IS NOT NULL AND encoded_dir != ''`\n )\n .all() as RegistryProject[];\n const map = new Map<string, string>();\n for (const row of rows) {\n map.set(row.encoded_dir, row.root_path);\n }\n return map;\n } catch {\n return new Map();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Top-level jsonl parser\n// ---------------------------------------------------------------------------\n\ninterface TopLevelInfo {\n systemLines: number;\n size: number;\n mtime: number;\n}\n\nfunction parseTopLevel(filePath: string): TopLevelInfo {\n let systemLines = 0;\n let size = 0;\n let mtime = 0;\n try {\n const st = statSync(filePath);\n size = st.size;\n mtime = st.mtimeMs;\n const content = readFileSync(filePath, \"utf8\");\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n if (t.includes('\"type\":\"system\"') || t.includes('\"type\": \"system\"')) {\n systemLines++;\n }\n }\n } catch {\n /* ignore */\n }\n return { systemLines, size, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Sessions/ transcript parser\n// ---------------------------------------------------------------------------\n\ninterface TranscriptInfo {\n userLines: number;\n lastUserPrompt: string;\n msgCount: number;\n aiTitle?: string;\n mtime: number;\n}\n\nfunction parseTranscript(filePath: string): TranscriptInfo {\n let userLines = 0;\n let lastUserPrompt = \"\";\n let msgCount = 0;\n let aiTitle: string | undefined;\n let mtime = 0;\n\n try {\n mtime = statSync(filePath).mtimeMs;\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n }\n\n let content: string;\n try {\n content = readFileSync(filePath, \"utf8\");\n } catch {\n return { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime };\n }\n\n for (const line of content.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n msgCount++;\n\n const hasUser = t.includes('\"type\":\"user\"') || t.includes('\"type\": \"user\"');\n const hasTitle =\n t.includes('\"type\":\"ai-title\"') || t.includes('\"type\": \"ai-title\"');\n if (!hasUser && !hasTitle) continue;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(t) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n if (parsed.type === \"ai-title\") {\n const v = parsed.title ?? parsed.name;\n if (typeof v === \"string\" && v.trim()) aiTitle = v.trim();\n continue;\n }\n\n if (parsed.type !== \"user\") continue;\n userLines++;\n\n const msg = parsed.message as Record<string, unknown> | undefined;\n if (!msg) continue;\n const c = msg.content;\n let text = \"\";\n if (typeof c === \"string\") {\n text = c;\n } else if (Array.isArray(c) && c.length > 0) {\n const first = c[0] as Record<string, unknown>;\n if (typeof first.text === \"string\") text = first.text;\n }\n if (text) lastUserPrompt = text.slice(0, 80).replace(/\\n/g, \" \");\n }\n\n return { userLines, lastUserPrompt, msgCount, aiTitle, mtime };\n}\n\n// ---------------------------------------------------------------------------\n// Main scanner\n// ---------------------------------------------------------------------------\n\nexport interface ScanOptions {\n /** Maximum results to return after sorting (default: 200). */\n limit?: number;\n /**\n * Filter mode:\n * \"named\" (default) — resumable OR in clc registry (named sessions the user cares about)\n * \"all\" — everything including unnamed orphans\n * \"resumable\" — only sessions claude --resume accepts (top-level + system lines)\n */\n filter?: \"named\" | \"all\" | \"resumable\";\n /**\n * @deprecated Use filter instead. When true → filter:\"resumable\", false → filter:\"named\".\n * Kept for backwards compatibility with callers that predate the filter field.\n */\n resumableOnly?: boolean;\n}\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\n\n// UUID format: 8-4-4-4-12 hex digits\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Compute the effective filter mode, resolving the legacy resumableOnly flag.\n */\nfunction resolveFilter(opts: ScanOptions): \"named\" | \"all\" | \"resumable\" {\n if (opts.filter) return opts.filter;\n if (opts.resumableOnly === true) return \"resumable\";\n if (opts.resumableOnly === false) return \"all\";\n return \"named\"; // default\n}\n\n/**\n * Scan ~/.claude/projects/ for all Claude Code sessions.\n *\n * Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).\n * Pass 1b: walk <project>/sessions/<uuid>.jsonl — where the stop hook moves a\n * session's transcript once it ends. These are resumable too, and\n * skipping them made every cleanly-stopped session unfindable by name.\n * Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.\n * For each such entry, scan the entry's project dir for the FRESHEST session\n * and attach the registry name to it. This fixes the stale-UUID bug where\n * clc's session.json points to an old uuid after a fresh start.\n * Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.\n *\n * Filter modes:\n * \"named\" — resumable + registry-known stubs + transcript-only (default)\n * \"all\" — all of the above + unnamed orphans from sessions/ subdirs\n * \"resumable\" — only sessions claude --resume accepts\n *\n * Returns results sorted by mtime descending.\n */\nexport function scanSessions(\n db: Database,\n opts: ScanOptions = {}\n): ScannedSession[] {\n const limit = opts.limit ?? 200;\n const filterMode = resolveFilter(opts);\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const clcInfoMap = buildClcInfoMap();\n const rootPathMap = buildRegistryRootPathMap(db);\n const results: ScannedSession[] = [];\n // Track which UUIDs we've already added (from the jsonl walk)\n const seenUuids = new Set<string>();\n // Track which clc names we've already attached to a session\n const attachedClcUuids = new Set<string>();\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n // Build a map: encodedDir → all top-level sessions found in Pass 1\n // This is used in Pass 2 to find the freshest session for a clc registry entry.\n const sessionsByEncodedDir = new Map<string, ScannedSession[]>();\n\n // ---- Pass 1: walk top-level <project>/<uuid>.jsonl files ----\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n const decodedPath =\n smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n const registryRootPath = rootPathMap.get(encodedDir);\n const projectBasename = basename(decodedPath);\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const clcInfo = clcInfoMap.get(uuid);\n const inRegistry = !!clcInfo;\n\n // Determine status\n const sessionStatus: SessionStatus = resumable\n ? \"resumable\"\n : inRegistry\n ? \"stub\"\n : \"orphan\";\n\n // Apply filter — but include ALL in this pass so Pass 2 can use them\n // We'll filter again when deciding what to add to `results`.\n const passesFilter =\n filterMode === \"resumable\"\n ? resumable\n : filterMode === \"named\"\n ? resumable || inRegistry\n : true;\n\n // Join with sessions/ transcript (best-effort)\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n // Use top-level mtime as primary\n const mtime = topInfo.mtime || transcript.mtime;\n\n // Name resolution: clc registry (verbatim) → ai-title → project basename\n const friendlyName =\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined;\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus,\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime,\n friendlyName,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n // Track in per-dir map for Pass 2 lookups (all sessions, regardless of filter)\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (inRegistry) attachedClcUuids.add(uuid);\n\n if (passesFilter) {\n results.push(session);\n }\n }\n\n // ---- Pass 1b: finished sessions, which live only in sessions/ ----\n //\n // The stop hook MOVES a session's transcript from <project>/<uuid>.jsonl to\n // <project>/sessions/<uuid>.jsonl. Pass 1 walks the top level only, so a\n // session becomes invisible to this scan the moment it is cleanly stopped —\n // and name resolution is built on this scan. That is why `pai Paperfull`\n // stopped finding Paperfull: three resumable transcripts sat in sessions/\n // while the scan reported zero sessions for the project, so `pai <name>`\n // fell through to a prompt-history search over old messages.\n //\n // These are reported resumable, which needs justifying, because\n // `claude --resume` does NOT read sessions/ — it reads the top-level path\n // and only that. What makes the label true is that nothing resumes without\n // going through probeResume first, and probeResume links the transcript back\n // to the top level before answering (lib/launch.ts, restoreTopLevel). PAI's\n // SessionStart hook displaced these files in the first place; it now links\n // rather than moves, so this is a claim about repairable state, not a hope.\n //\n // Do not weaken that chain without demoting these to transcript-only. An\n // earlier revision asserted plain \"claude --resume accepts these\", which is\n // false, and it fed unresumable ids to launch with full confidence.\n const sessionsDir = join(projectDir, \"sessions\");\n let finished: string[] = [];\n try {\n finished = readdirSync(sessionsDir);\n } catch {\n finished = []; // no sessions/ subdir — nothing was ever stopped here\n }\n\n for (const file of finished) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (seenUuids.has(uuid)) continue; // Pass 1 already has it\n\n const sessionJsonlPath = join(sessionsDir, file);\n const transcript = parseTranscript(sessionJsonlPath);\n const clcInfo = clcInfoMap.get(uuid);\n\n const session: ScannedSession = {\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n // No top-level file — that is the whole point of this pass.\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: true,\n sessionStatus: \"resumable\",\n sessionJsonlPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName:\n clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? undefined,\n clcDirectory: clcInfo?.directory,\n registryRootPath,\n };\n\n if (!sessionsByEncodedDir.has(encodedDir)) {\n sessionsByEncodedDir.set(encodedDir, []);\n }\n sessionsByEncodedDir.get(encodedDir)!.push(session);\n\n seenUuids.add(uuid);\n if (clcInfo) attachedClcUuids.add(uuid);\n\n results.push(session); // resumable — passes every filter mode\n }\n }\n\n // ---- Pass 2: clc registry entries whose cached UUID wasn't found as a top-level file ----\n // For each such entry, find the project's encodedDir and look for the FRESHEST\n // resumable session there. Attach the clc name to it.\n // This fixes the stale-UUID bug: clc may store a uuid that's no longer valid,\n // but the project dir has a new active session the user is actually in.\n if (filterMode !== \"resumable\") {\n for (const [cachedUuid, clcInfo] of clcInfoMap) {\n if (attachedClcUuids.has(cachedUuid)) continue; // already named in Pass 1\n\n // Derive the encodedDir from clcInfo.directory\n let foundEncodedDir: string | undefined;\n if (clcInfo.directory) {\n const real = realpathSyncSafe(clcInfo.directory);\n if (real) {\n const encoded = encodeProjectDir(real);\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) {\n foundEncodedDir = encoded;\n }\n }\n }\n\n // Fallback: check if the cached uuid exists as a sessions/ transcript somewhere\n if (!foundEncodedDir) {\n for (const encodedDir of encodedDirs) {\n if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, \"sessions\", `${cachedUuid}.jsonl`))) {\n foundEncodedDir = encodedDir;\n break;\n }\n }\n }\n\n if (foundEncodedDir) {\n const dirSessions = sessionsByEncodedDir.get(foundEncodedDir) ?? [];\n\n // Find the freshest RESUMABLE session in this dir that doesn't yet have a name\n const freshestResumable = dirSessions\n .filter((s) => s.resumable && !s.friendlyName)\n .sort((a, b) => b.mtime - a.mtime)[0];\n\n if (freshestResumable) {\n // Attach the clc name to this fresher session\n freshestResumable.friendlyName = clcInfo.name;\n freshestResumable.clcDirectory =\n freshestResumable.clcDirectory ?? clcInfo.directory;\n freshestResumable.sessionStatus = \"resumable\";\n attachedClcUuids.add(freshestResumable.uuid);\n\n // Add to results if not already there (it might have been filtered out as orphan)\n if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {\n // It was added to sessionsByEncodedDir but might have been filtered from results\n if (!results.includes(freshestResumable)) {\n results.push(freshestResumable);\n }\n }\n continue; // Done with this clc entry\n }\n\n // No unnamed resumable session found. The project might have resumable sessions\n // that already have a name (different name). Or only stubs/orphans.\n // Fall through to transcript-only handling below.\n }\n\n // ---- Pass 3: truly no top-level jsonl → transcript-only ----\n if (!foundEncodedDir) {\n // We couldn't find the project dir at all\n const encodedDir = \"\";\n const decodedPath = clcInfo.directory ?? cachedUuid;\n const registryRootPath = undefined;\n\n const transcript: TranscriptInfo = { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n const mtime = transcript.mtime;\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: \"\",\n msgCount: 0,\n mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n continue;\n }\n\n // Project dir was found but no unnamed resumable session.\n // Check sessions/ for the cached uuid transcript.\n const foundTranscriptPath = existsSync(\n join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n )\n ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, \"sessions\", `${cachedUuid}.jsonl`)\n : undefined;\n\n const decodedPath =\n clcInfo.directory ??\n (smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, \"/\"));\n const registryRootPath = rootPathMap.get(foundEncodedDir);\n const topLevelPath = join(\n CLAUDE_PROJECTS_DIR,\n foundEncodedDir,\n `${cachedUuid}.jsonl`\n );\n\n const transcript: TranscriptInfo = foundTranscriptPath\n ? parseTranscript(foundTranscriptPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n seenUuids.add(cachedUuid);\n results.push({\n uuid: cachedUuid,\n shortId: cachedUuid.slice(0, 8),\n encodedDir: foundEncodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: foundTranscriptPath,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: transcript.mtime,\n friendlyName: clcInfo.name,\n clcDirectory: clcInfo.directory,\n registryRootPath,\n });\n }\n }\n\n results.sort((a, b) => b.mtime - a.mtime);\n return results.slice(0, limit);\n}\n\n// ---------------------------------------------------------------------------\n// Path encoding helpers\n// ---------------------------------------------------------------------------\n\n/** Claude Code's project-dir encoding: replace / . - (space) with - */\nfunction encodeProjectDir(realPath: string): string {\n // Leading slash becomes leading -; all / . - and space → -\n return realPath.replace(/[/.\\- ]/g, \"-\");\n}\n\n/** realpathSync that returns null instead of throwing */\nfunction realpathSyncSafe(p: string): string | null {\n try {\n return realpathSync(p);\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Age formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtAge(mtime: number): string {\n const diffMs = Date.now() - mtime;\n const diffMin = Math.floor(diffMs / 60_000);\n const diffHr = Math.floor(diffMs / 3_600_000);\n const diffDay = Math.floor(diffMs / 86_400_000);\n\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay < 30) return `${diffDay}d`;\n return `${Math.floor(diffDay / 30)}mo`;\n}\n\n// ---------------------------------------------------------------------------\n// Resolve a name/id/prefix to a session entry\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedSession {\n session: ScannedSession;\n /** The friendly name to use verbatim in the /Name command. Never slugified. */\n friendlyName?: string;\n}\n\n/**\n * Filesystem-level UUID scan: walk ALL ~/.claude/projects/<encoded-dir>/<uuid>.jsonl\n * looking for top-level files whose UUID starts with `prefix`.\n *\n * Returns a ScannedSession-like object (minimal fields) for each match, or an empty\n * array if nothing is found. This is used as a fallback when the regular catalog\n * (limited to named/recent sessions) doesn't contain the requested UUID.\n *\n * Complexity: O(number of project directories + files per dir). Typically <200 dirs\n * with a handful of top-level jsonl each — fast enough for an interactive CLI.\n */\nfunction scanFilesystemForUuidPrefix(prefix: string): ScannedSession[] {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) return [];\n\n const prefixLower = prefix.toLowerCase();\n const matches: ScannedSession[] = [];\n\n let encodedDirs: string[];\n try {\n encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);\n } catch {\n return [];\n }\n\n for (const encodedDir of encodedDirs) {\n const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);\n try {\n if (!statSync(projectDir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n let files: string[];\n try {\n files = readdirSync(projectDir);\n } catch {\n continue;\n }\n\n for (const file of files) {\n if (!file.endsWith(\".jsonl\")) continue;\n const uuid = file.slice(0, -6);\n if (!UUID_RE.test(uuid)) continue;\n if (!uuid.toLowerCase().startsWith(prefixLower)) continue;\n\n const topLevelPath = join(projectDir, file);\n const topInfo = parseTopLevel(topLevelPath);\n const resumable = topInfo.systemLines > 0;\n const decodedPath = smartDecodeDir(encodedDir) ?? encodedDir.replace(/-/g, \"/\");\n\n // Check sessions/ transcript for prompt and title\n const sessionJsonlPath = join(projectDir, \"sessions\", `${uuid}.jsonl`);\n const hasTranscript = existsSync(sessionJsonlPath);\n const transcript: TranscriptInfo = hasTranscript\n ? parseTranscript(sessionJsonlPath)\n : { userLines: 0, lastUserPrompt: \"\", msgCount: 0, mtime: 0 };\n\n matches.push({\n uuid,\n shortId: uuid.slice(0, 8),\n encodedDir,\n decodedPath,\n topLevelPath,\n topLevelSystemLines: topInfo.systemLines,\n topLevelSize: topInfo.size,\n resumable,\n sessionStatus: resumable ? \"resumable\" : \"stub\",\n sessionJsonlPath: hasTranscript ? sessionJsonlPath : undefined,\n userLines: transcript.userLines,\n lastUserPrompt: transcript.lastUserPrompt,\n msgCount: transcript.msgCount,\n aiTitle: transcript.aiTitle,\n mtime: topInfo.mtime || transcript.mtime,\n friendlyName: transcript.aiTitle ?? basename(decodedPath),\n clcDirectory: undefined,\n registryRootPath: undefined,\n });\n }\n }\n\n return matches.sort((a, b) => b.mtime - a.mtime);\n}\n\n/**\n * Resolve a name-or-id-or-prefix to a single ScannedSession.\n *\n * Comparisons are case-insensitive; stored casing is preserved in output.\n *\n * Priority:\n * 1. Exact case-insensitive match on friendlyName\n * 2. Partial case-insensitive match (contains)\n * 3. UUID prefix match against the in-memory catalog\n * 4. UUID prefix match against the full filesystem (fallback for any session)\n */\nexport function resolveSessionByNameOrId(\n sessions: ScannedSession[],\n query: string\n): ResolvedSession {\n const qLower = query.toLowerCase().trim();\n\n // 1. Exact match (case-insensitive, preserve stored casing)\n const byExact = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower\n );\n if (byExact.length >= 1) {\n return { session: byExact[0], friendlyName: byExact[0].friendlyName };\n }\n\n // 2. Partial match\n const byPartial = sessions.filter(\n (s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower)\n );\n if (byPartial.length === 1) {\n return { session: byPartial[0], friendlyName: byPartial[0].friendlyName };\n }\n if (byPartial.length > 1) {\n const candidates = byPartial\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `Ambiguous name \"${query}\" — ${byPartial.length} matches:\\n${candidates}\\n\\nBe more specific or use a UUID prefix.`\n );\n }\n\n // 3. UUID prefix against catalog\n const byUuid = sessions.filter((s) => s.uuid.startsWith(qLower));\n if (byUuid.length === 1) {\n return { session: byUuid[0], friendlyName: byUuid[0].friendlyName };\n }\n if (byUuid.length > 1) {\n const candidates = byUuid\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${byUuid.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n\n // 4. Filesystem fallback — scan ALL ~/.claude/projects/*/<uuid>.jsonl files\n // This handles any session UUID, even if not in the named/recent catalog.\n // Only attempted when the query looks like a UUID prefix (hex + dashes).\n const UUID_PREFIX_RE = /^[0-9a-f-]{4,36}$/i;\n if (UUID_PREFIX_RE.test(qLower)) {\n const fsSessions = scanFilesystemForUuidPrefix(qLower);\n if (fsSessions.length === 1) {\n return { session: fsSessions[0], friendlyName: fsSessions[0].friendlyName };\n }\n if (fsSessions.length > 1) {\n const candidates = fsSessions\n .slice(0, 5)\n .map(\n (s, i) =>\n ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`\n )\n .join(\"\\n\");\n throw new Error(\n `UUID prefix \"${query}\" is ambiguous — ${fsSessions.length} matches:\\n${candidates}\\n\\nProvide more characters.`\n );\n }\n }\n\n throw new Error(\n `No session found matching \"${query}\".\\n\\nRun: pai sessions to list sessions.\\nRun: pai sessions --all to include transcript-only sessions.\\nRun: pai find <words> to search prompt history.`\n );\n}\n","/**\n * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.\n *\n * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,\n * and reads a single newline-terminated JSON response. No class needed —\n * just a thin async function matching the WatcherClient protocol.\n *\n * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawnSync } from \"node:child_process\";\nimport { aibrokerSocketPath } from \"../../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Lightweight session metadata returned by the AIBroker `sessions` IPC method.\n * Does NOT include scrollback content — use `session_content` for that.\n */\nexport interface AiBrokerSessionMeta {\n index: number;\n sessionId: string;\n /** iTerm2 tab title or profile name */\n name: string;\n /** PAI session name set via /Name; null for bare shells */\n paiName: string | null;\n atPrompt: boolean;\n /** \"claude\" for Claude Code panes, \"shell\" for bare terminals */\n kind: \"claude\" | \"shell\";\n /** Whether this is the currently focused pane */\n active: boolean;\n /** Last user prompt seen in scrollback (only populated by fetchLiveSessionsWithPrompts) */\n lastPrompt?: string;\n}\n\ninterface AiBrokerSessionsResult {\n sessions: AiBrokerSessionMeta[];\n}\n\n// ---------------------------------------------------------------------------\n// Core call\n// ---------------------------------------------------------------------------\n\n// AIBROKER_SOCKET stays first: it is AIBroker's own variable and a user who has\n// set it means it. The fallback goes through runtime-paths so the test guard\n// can redirect it — writing to a live socket from a test is the same hazard as\n// writing to live user state, and this one is not even ours.\nconst DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();\n\n/**\n * How long to wait for `send_to_session`, which must exceed the server's own\n * ack window or the caller gives up while the callee is still working.\n *\n * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —\n * the text leaving the input line — before answering. The generic client\n * timeout is 8s. So every send needing 8-15s to confirm reported\n * \"AIBroker IPC call timed out\" while succeeding, and the handler's eventual\n * `delivered: true` was written into a socket nobody was reading any more.\n *\n * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them\n * verifiably paused. A caller's deadline shorter than the callee's is not a\n * tuning question, it is a guaranteed false negative on every slow success.\n */\nconst SEND_TIMEOUT_MS = 30_000;\n\n/**\n * Call an AIBroker IPC method and return the result.\n *\n * Resolves with the `result` field of a successful response.\n * Rejects if the socket is not available, the call times out, or the\n * daemon returns an error.\n *\n * @param method IPC method name (e.g. \"session_content\", \"send_to_session\")\n * @param params Method parameters object\n * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)\n */\nexport function callAiBroker(\n method: string,\n params: Record<string, unknown> = {},\n timeoutMs = 8_000\n): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const socketPath = DEFAULT_SOCKET;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(err: Error | null, value?: Record<string, unknown>): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket.destroy();\n } catch {\n /* ignore */\n }\n if (err) reject(err);\n else resolve(value!);\n }\n\n const socket = connect(socketPath, () => {\n const request = {\n id: randomUUID(),\n sessionId: process.env.TERM_SESSION_ID ?? \"pai-cli\",\n method,\n params,\n };\n const itermId = process.env.ITERM_SESSION_ID;\n if (itermId) Object.assign(request, { itermSessionId: itermId });\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n\n let response: { ok: boolean; result?: Record<string, unknown>; error?: string };\n try {\n response = JSON.parse(line);\n } catch {\n finish(new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"AIBroker IPC call failed\"));\n } else {\n finish(null, response.result ?? {});\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(new Error(\"AIBroker not running (socket not found).\"));\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) finish(new Error(\"AIBroker IPC connection closed before response.\"));\n });\n\n timer = setTimeout(\n () => finish(new Error(\"AIBroker IPC call timed out.\")),\n timeoutMs\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Typed helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.\n * Returns an empty array if AIBroker is not running.\n *\n * This is metadata-only (no scrollback). It is faster than `session_content`\n * and the correct source for listing/routing purposes.\n */\nexport async function fetchLiveSessions(): Promise<AiBrokerSessionMeta[]> {\n try {\n const result = await callAiBroker(\"sessions\", {});\n const sessions = (result as unknown as AiBrokerSessionsResult).sessions;\n if (!Array.isArray(sessions)) return [];\n return sessions;\n } catch {\n return [];\n }\n}\n\n/**\n * Fetch live sessions WITH the last user prompt extracted from terminal scrollback.\n *\n * Heavier than `fetchLiveSessions` (one IPC call returning full content for\n * every session), but yields a `lastPrompt` field useful for the unified\n * listing.\n */\nexport async function fetchLiveSessionsWithPrompts(): Promise<\n (AiBrokerSessionMeta & { lastPrompt?: string })[]\n> {\n // Get the basic metadata first (always works, fast).\n const metas = await fetchLiveSessions();\n if (metas.length === 0) return [];\n\n // Then enrich with last prompts via session_content (single call, all sessions).\n try {\n const contentResult = (await callAiBroker(\"session_content\", { lines: 60 })) as {\n sessions?: Array<{ sessionId: string; content?: string }>;\n };\n const contentMap = new Map<string, string>();\n for (const s of contentResult.sessions ?? []) {\n if (s.sessionId && typeof s.content === \"string\") {\n contentMap.set(s.sessionId, s.content);\n }\n }\n return metas.map((m) => {\n const content = contentMap.get(m.sessionId);\n const lastPrompt = content ? extractLastUserPrompt(content) : undefined;\n return { ...m, lastPrompt };\n });\n } catch {\n return metas.map((m) => ({ ...m }));\n }\n}\n\n/**\n * Extract the most-recent user prompt from a terminal scrollback string.\n *\n * Claude Code's TUI shows user input lines prefixed with `❯ `. We scan from\n * the bottom up, skipping the active input box (between the two horizontal\n * rule lines) and the statusline footer.\n */\nfunction extractLastUserPrompt(content: string): string | undefined {\n const lines = content.split(\"\\n\");\n // Walk bottom-up looking for the most recent user-typed line.\n // Claude Code marks user prompts with one of these prefixes:\n // ❯ <text> (current/recent prompt in the input box or scrollback)\n // > <text> (older variant)\n // The line MUST have non-empty content after the prompt symbol.\n // Skip the active input box (often empty `❯`) and statusline (👋 PAI CC...).\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n // Match ❯ or > (followed by space) then content\n const m = line.match(/^[❯>]\\s+(.+?)\\s*$/);\n if (!m) continue;\n let text = m[1].trim();\n if (!text) continue;\n // Skip lines that are just box-drawing or known UI noise\n if (/^[─━═]+/.test(text)) continue;\n if (text.startsWith(\"👋\")) continue;\n // Strip ANSI escape sequences that may leak in\n text = text.replace(/\\x1B\\[[0-9;]*[A-Za-z]/g, \"\").trim();\n if (text) return text;\n }\n return undefined;\n}\n\n/**\n * Send text to a specific AIBroker session by its iTerm2 sessionId.\n *\n * The wire keys are `target` and `message`, and neither is negotiable — the\n * handler rejects anything else with \"target is required\" before it looks at\n * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of\n * THIS function's own parameters rather than the shape of the IPC, so every\n * call failed identically. `pai pause all` was the only caller, so the live\n * path had never once executed: its --dry-run branch returns before sending,\n * and that was the only branch anyone had exercised.\n *\n * The caller must NOT terminate `text` with a newline. The transport sends with\n * `enter: true` and appends the Enter itself, so a trailing \\n submits twice —\n * once for the text and once for an empty prompt.\n */\nexport async function sendToSession(\n sessionId: string,\n text: string,\n timeoutMs = SEND_TIMEOUT_MS\n): Promise<{ ok: boolean; error?: string; timedOut?: boolean }> {\n try {\n await callAiBroker(\"send_to_session\", { target: sessionId, message: text }, timeoutMs);\n return { ok: true };\n } catch (e) {\n const error = String(e);\n // A timeout is NOT a delivery failure, and conflating the two is expensive.\n // The handler deposits into the target's mailbox BEFORE it waits for the\n // submit ack, so a send that times out has still been delivered — only the\n // confirmation is missing. Reported as a plain failure, the natural response\n // is to send again, which is how `pai pause all` produced nested\n // \"carried forward\" blocks in checkpoints that had already been written.\n return { ok: false, error, timedOut: /timed out/i.test(error) };\n }\n}\n\n/**\n * Switch iTerm2 focus to the session identified by `target`.\n *\n * `target` can be a sessionId, paiName, or tab index number (as string).\n * After switching, activates the iTerm2 application itself so the window\n * comes to the foreground.\n *\n * Returns { ok: true } if AIBroker confirmed the switch, or\n * { ok: false, error } if AIBroker is not running or the session was not found.\n */\nexport async function switchToSession(target: string): Promise<{ ok: boolean; error?: string }> {\n try {\n await callAiBroker(\"switch\", { target });\n // Bring iTerm2 itself to the foreground (the IPC only selects the tab)\n const { spawnSync } = await import(\"node:child_process\");\n spawnSync(\"osascript\", [\"-e\", 'tell application \"iTerm\" to activate'], {\n stdio: \"ignore\",\n });\n return { ok: true };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n\n/**\n * Bring the iTerm2 tab containing a specific session to the front.\n *\n * This is the mechanism AIBroker's screenshot path uses: match the session by\n * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which\n * is iTerm's own `id of session`), then `select` its window, tab, and session.\n * Unlike the `switch` IPC (which only flips an internal index and never touches\n * iTerm), this actually reveals the tab.\n */\nexport function revealItermSession(itermSessionId: string): { ok: boolean; error?: string } {\n // Strip any \"iterm:\" style prefix, matching AIBroker's stripItermPrefix.\n const id = itermSessionId.replace(/^iterm:/i, \"\").trim();\n const script = `tell application \"iTerm2\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if id of s is \"${id}\" then\n select w\n select t\n select s\n return \"ok\"\n end if\n end repeat\n end repeat\n end repeat\n return \"not-found\"\nend tell`;\n try {\n const r = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n if (r.status !== 0) {\n return { ok: false, error: (r.stderr || \"osascript failed\").trim() };\n }\n if ((r.stdout ?? \"\").trim() === \"ok\") return { ok: true };\n return { ok: false, error: \"session not found in any iTerm2 window\" };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n","/**\n * Print the working directory of a `claude` session that pai launched, right\n * after it exits. This lets the user cd back into the same directory and\n * restart claude to continue — Claude Code's own `--resume` hint is unreliable\n * for this workflow.\n *\n * Why here and not a SessionEnd hook: a hook inside Claude Code is cancelled\n * during teardown (anthropics/claude-code#41577) and races the terminal\n * restore, so it can't reliably print anything. pai spawns the `claude` binary\n * directly (no shell), so the `claude()` shell wrapper never sees pai-launched\n * sessions either. Printing here — after spawnSync has returned and claude has\n * fully exited — is the one place that reliably reaches the terminal.\n */\nexport function printExitDir(dir: string): void {\n process.stdout.write(\n `\\n\\x1b[2m📂 Working directory:\\x1b[0m ${dir}\\n` +\n `\\x1b[2m cd \"${dir}\"\\x1b[0m\\n`,\n );\n}\n","/**\n * launch.ts — Launch Claude Code in a directory, in the CURRENT terminal.\n *\n * Shared by the interactive picker (pick.ts). Deliberately does NOT switch\n * iTerm tabs (aibroker_switch): switching jumps the user to a different — and\n * sometimes wrong — terminal, which is confusing. Picking a place should start\n * a session right here, in the chosen directory.\n *\n * Behaviour:\n * engine → fresh launches follow the workers config: routing on\n * starts an interactive `pai worker run` (the glm-shim\n * shape) instead of claude. `engine` in opts forces one;\n * resume always stays claude.\n * resume-or-fresh → if a resumable UUID is given, probe it; on success\n * `claude --resume`, otherwise fall back to a fresh session\n * in the same dir. With no UUID, start fresh.\n *\n * The `claude` child inherits the tty (stdio: \"inherit\"), so the session runs\n * in the terminal that launched `pai`. On exit we print the working directory.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport {\n realpathSync,\n existsSync,\n linkSync,\n copyFileSync,\n openSync,\n readSync,\n closeSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport { err } from \"../utils.js\";\nimport { readWorkersSection, type WorkersConfig } from \"../../workers/config.js\";\nimport { printExitDir } from \"./exit-dir.js\";\n\nexport interface ProbeResult {\n ok: boolean;\n reason?: string;\n}\n\n/**\n * Put a transcript back where `claude --resume` looks for it.\n *\n * `claude --resume <uuid>` reads ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl\n * and ONLY that path. A copy under `sessions/` is invisible to it — measured\n * 2026-08-04, both directions:\n *\n * b3462801 867 KB, sessions/ only → \"No conversation found with session ID\"\n * a9ecdc1c top level → found\n *\n * Location is one of two factors, and this function addresses that one. PAI\n * displaced these files itself, from FOUR movers — a SessionStart hook, a\n * UserPromptSubmit hook, the stop hook, and the work-queue worker — of which\n * the UserPromptSubmit one did most of the damage, because it ran on every\n * prompt of every session and excluded only the caller's own transcript. All\n * four now hardlink (project-utils/paths.ts). So this restores a file PAI\n * displaced rather than inventing a layout Claude Code does not use.\n *\n * The other factor is content, and no amount of relinking helps there — see\n * `hasConversation`.\n *\n * A hard link is preferred over a copy: same inode, no second megabyte on disk,\n * and the archive under `sessions/` keeps working for everything that reads it.\n * Returns whether the top-level path exists afterwards.\n */\nexport function restoreTopLevel(uuid: string, dir: string): boolean {\n const topLevel = join(dir, `${uuid}.jsonl`);\n if (existsSync(topLevel)) return true;\n\n const archived = join(dir, \"sessions\", `${uuid}.jsonl`);\n if (!existsSync(archived)) return false;\n\n try {\n linkSync(archived, topLevel);\n return true;\n } catch {\n try {\n copyFileSync(archived, topLevel);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Does this transcript hold an exchange, or only session metadata?\n *\n * The second reason `claude --resume` says \"No conversation found\": the file is\n * there and readable and still holds no conversation. Measured 2026-08-04 on\n * 046bb712 — 537 bytes of last-prompt, custom-title, agent-name, mode and\n * permission-mode, no user line, no assistant line. It was restored to the top\n * level, verified same-inode, and still refused. It was never resumable, and\n * relinking cannot make it so. 30 of PAI's own 50 displaced transcripts are\n * this shape, one of them 745 KB — size proves nothing, because hook context\n * attachments are large.\n *\n * This scans the WHOLE file, and a bounded head will not do. It is tempting —\n * \"a real session's first assistant line lands within the first few KB\" — and\n * it is false. Measured on b3462801, a session `claude --resume` accepts:\n *\n * file size 866953\n * length of LINE 1 762977 <- one hook context attachment\n * first \"type\":\"user\" 766830\n *\n * The first exchange sits past 766 KB because line 1 is a single enormous\n * attachment blob. Any head shorter than that reports a 867 KB working session\n * as an empty stub, which is the exact false negative this function exists to\n * prevent — and it is the shape that produced today's whole incident.\n *\n * Chunked so that a large transcript costs a scan rather than a resident copy,\n * with an overlap so the marker cannot hide across a chunk boundary. Reading a\n * few MB to answer a question about resumability is cheap; being wrong is not.\n *\n * Unreadable counts as real. Claiming a session is empty when it cannot be\n * inspected would talk the caller out of a resume that might have worked.\n */\nconst CONVERSATION_MARKERS = ['\"type\":\"assistant\"', '\"type\": \"assistant\"', '\"type\":\"user\"', '\"type\": \"user\"'];\nconst OVERLAP = 32; // > longest marker, so none can straddle two chunks\n\nexport function hasConversation(path: string, chunkBytes = 1 << 20): boolean {\n let fd: number | undefined;\n try {\n fd = openSync(path, \"r\");\n const buf = Buffer.alloc(chunkBytes);\n let carry = \"\";\n let pos = 0;\n\n for (;;) {\n const read = readSync(fd, buf, 0, chunkBytes, pos);\n if (read <= 0) return false;\n pos += read;\n\n const text = carry + buf.subarray(0, read).toString(\"utf8\");\n if (CONVERSATION_MARKERS.some((m) => text.includes(m))) return true;\n carry = text.slice(-OVERLAP);\n }\n } catch {\n return true;\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n /* nothing useful to do */\n }\n }\n }\n}\n\n/**\n * Can this session be resumed — and if the only thing standing in the way is\n * where its transcript sits, put it back so that the answer is yes.\n *\n * Returns true/false when it can tell, and null when it cannot — the caller\n * must treat null as \"ask something else\", never as \"no\". A path this function\n * fails to recognise would otherwise silently veto a perfectly good resume.\n *\n * Claude Code stores transcripts at ~/.claude/projects/<encoded-cwd>/, where the\n * encoding replaces every non-alphanumeric character with `-`.\n */\nfunction transcriptOnDisk(\n uuid: string,\n cwd: string,\n home = homedir()\n): true | \"missing\" | \"stub\" | null {\n try {\n const encoded = cwd.replace(/[^a-zA-Z0-9]/g, \"-\");\n const dir = join(home, \".claude\", \"projects\", encoded);\n if (!existsSync(dir)) return null; // unknown layout — not evidence of absence\n if (!restoreTopLevel(uuid, dir)) return \"missing\";\n // Present is not the same as resumable. A metadata stub survives every\n // location check and still fails at `claude --resume`, and this probe is the\n // only thing standing between that and a caller that exits on the failure\n // instead of falling back to a fresh session.\n return hasConversation(join(dir, `${uuid}.jsonl`)) ? true : \"stub\";\n } catch {\n return null;\n }\n}\n\n/**\n * Probe whether a session UUID is resumable from `cwd`.\n *\n * The filesystem is asked first, and usually answers.\n *\n * This used to run `claude --resume <uuid> --print --output-format=json \"_\"`\n * with a 5s timeout, which is not a probe: it resumes the session AND sends a\n * prompt to the model, then waits for a complete JSON reply. That costs a model\n * round-trip per probe, and 5s is not enough time for one — for a LARGE session\n * least of all, because the transcript has to be loaded first.\n *\n * So the check failed precisely for the sessions most worth resuming, and the\n * caller's fallback quietly started a fresh session in their place. Observed\n * 2026-08-04: `pai Paperfull` reported `spawn error: spawnSync claude ETIMEDOUT`\n * for fb76a6c3 and started over, while\n * `~/.claude/projects/…-Paperfull/sessions/fb76a6c3-….jsonl` sat on disk the\n * whole time.\n *\n * A transcript on disk is what resumable MEANS, and reading a directory entry is\n * free. The spawn remains only for the case the filesystem cannot answer, and\n * now gets a timeout that a real answer can fit inside.\n *\n * \"On disk\" had to be tightened once. It first accepted a transcript sitting\n * only under `sessions/`, which claude --resume does not read — so the probe\n * swapped a 5s false negative for a confident false positive, and the caller\n * spawned a resume that died with \"No conversation found\" instead of falling\n * back to a fresh session. `restoreTopLevel` is what makes the permissive\n * reading true rather than merely optimistic.\n */\nexport function probeResume(uuid: string, cwd: string, home?: string): ProbeResult {\n const onDisk = transcriptOnDisk(uuid, cwd, home);\n if (onDisk === true) return { ok: true };\n if (onDisk === \"missing\") {\n return { ok: false, reason: \"No transcript on disk for this UUID\" };\n }\n if (onDisk === \"stub\") {\n // Say which of the two it is. \"No conversation found\" from claude tells the\n // user nothing about whether a repair might help; this does.\n return { ok: false, reason: \"Transcript holds only session metadata — no conversation to resume\" };\n }\n\n const result = spawnSync(\n \"claude\",\n [\"--resume\", uuid, \"--print\", \"--output-format=json\", \"_\"],\n { cwd, timeout: 30_000, env: process.env, stdio: [\"ignore\", \"ignore\", \"pipe\"] }\n );\n\n if (result.error) return { ok: false, reason: `spawn error: ${result.error.message}` };\n\n const stderr = result.stderr?.toString(\"utf8\") ?? \"\";\n if (\n stderr.toLowerCase().includes(\"no conversation found\") ||\n stderr.toLowerCase().includes(\"session not found\")\n ) {\n return { ok: false, reason: \"No conversation found for this UUID\" };\n }\n if (result.status !== 0) {\n return {\n ok: false,\n reason: `claude exited ${result.status ?? \"signal\"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : \"\"}`,\n };\n }\n return { ok: true };\n}\n\nexport interface LaunchOpts {\n /** If set, try to resume this session before falling back to fresh. */\n resumableUuid?: string;\n /** Skip the resume probe and start a brand-new session in the dir. */\n forceFresh?: boolean;\n /** Print what would happen, then return without launching. */\n dryRun?: boolean;\n /** \"auto\" follows the workers config; \"worker\"/\"claude\" force one engine. */\n engine?: \"auto\" | \"worker\" | \"claude\";\n}\n\n/** Which engine a launch runs on, and whether it resumes. */\nexport interface LaunchRoute {\n engine: \"worker\" | \"claude\";\n resume: boolean;\n}\n\n/**\n * Decide the engine for a launch. \"auto\" mirrors the gate the Agent-routing\n * hook uses, so picker and hook never disagree about whether routing is on.\n * An explicit engine wins outright — the picker w/a keys rely on that.\n *\n * Resume stays claude even under a provider: `pai worker run` always starts\n * fresh, so a claude transcript has nothing to resume on. The provider\n * default therefore applies to fresh launches only — a wanted resume pins\n * the engine to claude unless engine \"worker\" is forced, which drops the\n * resume outright.\n */\nexport function resolveLaunchRoute(\n workers: Pick<WorkersConfig, \"enabled\" | \"active\" | \"providers\">,\n opts: LaunchOpts\n): LaunchRoute {\n const routingOn =\n workers.enabled &&\n workers.active !== null &&\n Object.keys(workers.providers).length > 0;\n const wantsResume = !opts.forceFresh && !!opts.resumableUuid;\n const engine: \"worker\" | \"claude\" =\n opts.engine === \"worker\" ||\n (opts.engine !== \"claude\" && routingOn && !wantsResume)\n ? \"worker\"\n : \"claude\";\n return { engine, resume: engine === \"claude\" && wantsResume };\n}\n\n/**\n * Interactive worker run, same shape as the ~/.local/bin/glm shim: a label,\n * the directory, and no prompt — the run names itself from the label/dir.\n */\nexport function workerRunArgv(label: string, cwd: string): string[] {\n return [\"worker\", \"run\", \"--label\", label, \"--cwd\", cwd];\n}\n\n/**\n * Launch `claude` in `dir` in the current terminal. `name` is used for both the\n * Claude session label (--name) and the /Name slash command (tab/statusline).\n * Never returns on the live path — it exits the process after claude exits.\n */\nexport function launchInDir(dir: string, name: string, opts: LaunchOpts = {}): void {\n let cwd: string;\n try {\n cwd = realpathSync(dir);\n } catch {\n console.error(\n err(\n `Directory does not exist or cannot be resolved:\\n ${dir}\\n` +\n ` The folder may have moved or been deleted.`\n )\n );\n process.exitCode = 1;\n return;\n }\n\n const promptArg = `/Name ${name}\\ngo`;\n\n // A broken workers config must degrade to claude, never crash the picker.\n let workers: Pick<WorkersConfig, \"enabled\" | \"active\" | \"providers\"> = {\n enabled: false,\n active: null,\n providers: {},\n };\n try {\n workers = readWorkersSection().workers;\n } catch {\n /* routing stays off for this launch */\n }\n const route = resolveLaunchRoute(workers, opts);\n const wantResume = route.resume;\n\n if (opts.dryRun) {\n if (route.engine === \"worker\") {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (WORKER path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` argv: pai worker run --label \"${name}\" --cwd ${cwd}`);\n console.log();\n return;\n }\n if (wantResume) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would probe then exec (RESUME path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` probe: transcript on disk for ${opts.resumableUuid!.slice(0, 8)}?`);\n console.log(` argv: claude --resume ${opts.resumableUuid} --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n console.log(` fallback: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n } else {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would exec (FRESH path):\") + \"\\n\");\n console.log(` cwd: ${chalk.cyan(cwd)}`);\n console.log(` argv: claude --name \"${name}\" \"/Name ${name}\\\\ngo\"`);\n }\n console.log();\n return;\n }\n\n // Worker path: interactive `pai worker run`, fresh by design — no resume.\n if (route.engine === \"worker\") {\n const result = spawnSync(\"pai\", workerRunArgv(name, cwd), {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n if (result.error) {\n console.error(err(`Failed to launch pai worker run: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n return;\n }\n\n const fresh = () => {\n const result = spawnSync(\"claude\", [\"--name\", name, promptArg], {\n cwd,\n stdio: \"inherit\",\n env: process.env,\n });\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n };\n\n if (wantResume) {\n const probe = probeResume(opts.resumableUuid!, cwd);\n if (probe.ok) {\n const result = spawnSync(\n \"claude\",\n [\"--resume\", opts.resumableUuid!, \"--name\", name, promptArg],\n { cwd, stdio: \"inherit\", env: process.env }\n );\n if (result.error) {\n console.error(err(`Failed to launch claude: ${result.error.message}`));\n process.exitCode = 1;\n return;\n }\n printExitDir(cwd);\n process.exitCode = result.status ?? 0;\n return;\n }\n process.stderr.write(\n chalk.yellow(\n `\\n Resume failed for ${opts.resumableUuid!.slice(0, 8)}: ${probe.reason ?? \"unknown error\"}\\n` +\n ` Starting fresh session in same directory.\\n\\n`\n )\n );\n fresh();\n return;\n }\n\n fresh();\n}\n","/**\n * history-search.ts\n *\n * Content-based search across ~/.claude/history.jsonl.\n * Streams the file (avoids loading 26k+ lines into memory), groups matching\n * lines by sessionId, and returns results sorted by most-recent match.\n *\n * Used by: pai <query> (main resolver) and pai find (compat alias)\n */\n\nimport { existsSync, createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HistoryEntry {\n display?: string;\n timestamp?: number;\n project?: string;\n sessionId?: string;\n pastedContents?: unknown;\n}\n\nexport interface SessionMatch {\n sessionId: string | null;\n /** Most-recent timestamp among matching lines */\n lastMatchTs: number;\n /** Most-recent matching prompt text */\n lastMatchDisplay: string;\n /** Project path (from the most-recent match) */\n project: string;\n /** Number of matching lines in this session */\n matchCount: number;\n}\n\n// ---------------------------------------------------------------------------\n// History file location\n// ---------------------------------------------------------------------------\n\nexport const HISTORY_FILE = join(homedir(), \".claude\", \"history.jsonl\");\n\n// ---------------------------------------------------------------------------\n// Core search function (streaming)\n// ---------------------------------------------------------------------------\n\n/**\n * Search ~/.claude/history.jsonl for prompts matching the query (case-insensitive\n * substring). Results are grouped by sessionId and sorted by most-recent match.\n *\n * Entries without a sessionId (old Claude Code versions) are excluded from\n * results since they can't be resumed.\n */\nexport async function searchHistory(\n query: string,\n maxResults: number\n): Promise<SessionMatch[]> {\n if (!existsSync(HISTORY_FILE)) return [];\n\n const queryLower = query.toLowerCase();\n const bySession = new Map<string, SessionMatch>();\n\n const rl = createInterface({\n input: createReadStream(HISTORY_FILE, { encoding: \"utf8\" }),\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n const t = line.trim();\n if (!t) continue;\n\n let entry: HistoryEntry;\n try {\n entry = JSON.parse(t) as HistoryEntry;\n } catch {\n continue;\n }\n\n const display = entry.display ?? \"\";\n if (!display.toLowerCase().includes(queryLower)) continue;\n\n const ts = entry.timestamp ?? 0;\n const project = entry.project ?? \"\";\n const sessionId = entry.sessionId ?? null;\n\n // Skip entries with no sessionId — can't resume them\n if (!sessionId) continue;\n\n const existing = bySession.get(sessionId);\n if (!existing) {\n bySession.set(sessionId, {\n sessionId,\n lastMatchTs: ts,\n lastMatchDisplay: display,\n project,\n matchCount: 1,\n });\n } else {\n existing.matchCount++;\n if (ts > existing.lastMatchTs) {\n existing.lastMatchTs = ts;\n existing.lastMatchDisplay = display;\n existing.project = project;\n }\n }\n }\n\n // Sort by most-recent match descending\n const sorted = [...bySession.values()].sort(\n (a, b) => b.lastMatchTs - a.lastMatchTs\n );\n\n return sorted.slice(0, maxResults);\n}\n","/**\n * dedup-sessions.ts — Shared dedup catalog for `pai` and listing commands.\n *\n * Both `pai` (no args) and any listing command must show IDENTICAL output.\n * This module is the single source of truth for:\n * - Name normalization (strips spinner prefixes and \" (node)\" suffixes)\n * - Dedup grouping (one row per normalized name)\n * - Priority ranking (live > resumable > transcript-only > stub > project > orphan)\n * - Registered projects (shown as status \"idle\" when no live/disk session matches)\n * - Filtering (hide 0-session / cold / archived projects by default)\n * - Table rendering (shared columns and formatting)\n *\n * Default visibility rules (overridden by showAll=true):\n * SHOW: live sessions (always)\n * SHOW: resumable sessions (always)\n * SHOW: registered projects with session_count > 0 AND last_active within 90 days\n * OR session_count >= 3 (i.e. well-used projects regardless of age)\n * HIDE: projects with session_count = 0 (never used)\n * HIDE: projects with last_active > 90 days AND session_count < 3 (cold)\n * HIDE: archived projects (status != 'active')\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport chalk from \"chalk\";\nimport { renderTable, dim, header, warn } from \"../utils.js\";\nimport { fmtAge, type ScannedSession } from \"./session-scan.js\";\nimport { type AiBrokerSessionMeta } from \"./aibroker-client.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type UnifiedStatus =\n | \"live\"\n | \"resumable\"\n | \"transcript-only\"\n | \"stub\"\n | \"project\"\n | \"orphan\";\n\nexport const STATUS_PRIORITY: Record<UnifiedStatus, number> = {\n live: 0,\n resumable: 1,\n \"transcript-only\": 2,\n stub: 3,\n project: 4,\n orphan: 5,\n};\n\nexport interface UnifiedSession {\n /** Clean display name (normalized — no spinners, no \"(node)\") */\n name: string;\n /** Slug from the project registry (for slug-based matching in `pai <name>`) */\n slug?: string;\n status: UnifiedStatus;\n /** Only present for live sessions */\n liveSessionId?: string;\n /** Only present for disk sessions */\n diskSession?: ScannedSession;\n lastActivity: number;\n project: string;\n lastPrompt: string;\n /** For project-source entries: number of historical sessions ever */\n sessionCount?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Name normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Strip Claude Code spinner characters, decoration prefixes, and \" (node)\" suffixes\n * from a session name so that \"✳ Chenarlier (node)\" and \"Chenarlier\" group together.\n *\n * Handles:\n * - Unicode braille spinner frames: ⠐ ⠂ ⠁ ⠈ ⠘ ⠙ ⠚ ⠛ ⠗ ⠝\n * - Decoration characters: ✻ ✳ ✲ ✼ ✦ * ★ ☆ • ●\n * - Any leading non-letter/digit characters followed by whitespace\n * - Trailing \" (node)\" suffix (Claude Code appends this on some platforms)\n */\nexport function normalizeName(s: string): string {\n return s\n // Strip leading spinner / decoration characters (braille + symbols)\n .replace(/^[⠀-⣿✀-➿✻✳✲✼✦*★☆•●⠐⠂⠁⠈⠘⠙⠚⠛⠗⠝]+\\s*/u, \"\")\n // Strip any remaining leading non-alphanumeric prefix before whitespace\n .replace(/^[^a-zA-Z0-9À-ÿ -鿿]+\\s*/u, \"\")\n // Strip trailing \" (node)\" suffix\n .replace(/\\s*\\(node\\)\\s*$/i, \"\")\n .trim();\n}\n\n/**\n * Normalize a slug (kebab-case) to a human-readable form for matching.\n * \"jobs-beta\" → \"jobs beta\"\n */\nfunction slugToWords(slug: string): string {\n return slug.replace(/-/g, \" \");\n}\n\n// ---------------------------------------------------------------------------\n// Dedup builder\n// ---------------------------------------------------------------------------\n\nexport interface RegisteredProject {\n slug: string;\n display_name: string;\n root_path: string;\n session_count: number;\n last_active: number | null;\n status: string;\n}\n\n/**\n * Merge live (AIBroker) + disk (session-scan) + registry (projects DB) sessions\n * into a deduped catalog.\n *\n * Algorithm:\n * 1. Normalize names before grouping\n * 2. Group by normalized name (case-insensitive)\n * 3. Within each group, rank by STATUS_PRIORITY (live wins)\n * 4. Within same priority, keep the most recent (highest mtime)\n * 5. Sort output: by priority, then by lastActivity desc\n * 6. Filter idle project entries (default) or show all (showAll=true)\n *\n * @param showAll When true, include cold / zero-session / archived projects.\n */\n/**\n * Which of two sessions of equal status should represent a name.\n *\n * Recency alone was the rule, and recency is exactly the wrong instinct here.\n * A resume that FAILS still creates a session file, and that file is newer than\n * the transcript it failed to open. So the tie-break reliably handed back the\n * artefact of a bug in preference to the work.\n *\n * Measured on Paperfull, 2026-08-04:\n *\n * b3462801 867 KB 32 lines 3 user 15:41:57 <- the actual work\n * 7fdbb9a8 82 KB 25 lines 2 user 15:41:59 <- empty, Ctrl-C'd, WON by 2s\n * a9ecdc1c 82 KB 25 lines 2 user 15:49:40 <- a second failed resume\n *\n * `pai Paperfull` therefore resolved to 7fdbb9a8 — 82 KB of hook context in\n * `attachment` lines, with no assistant turn anywhere in it — while 867 KB of\n * real work sat one second older and unchosen.\n *\n * WHAT THIS DOES NOT FIX, measured before claiming otherwise: the reported\n * failure was `No conversation found with session ID: 7fdbb9a8`, and that error\n * is about WHERE the transcript is, not what is in it. Both transcripts live\n * only in sessions/, and `claude --resume b3462801` — the good one, the one this\n * change now selects — fails with the identical message. Verified directly, and\n * again on a second project. So this stops dedup preferring the artefact of a\n * bug, which is worth doing on its own, and `pai <Name>` still cannot resume a\n * cleanly-stopped session until the sessions/ relocation is addressed. That part\n * is tracked in Notes/TODO.md and belongs to session-scan/the stop hook.\n *\n * Conversation volume comes first because an empty artefact is small BY\n * CONSTRUCTION — nobody spoke into it — whereas an artefact can be newer by\n * mere milliseconds. Recency only decides between transcripts that recorded the\n * same amount, where it is a reasonable guess again.\n *\n * HONEST LIMIT: the exact signal is \"did the assistant ever reply\", and this is\n * not that — it is a proxy that happens to separate the measured case by one\n * user line and seven total lines, which is a thin margin. The real predicate\n * belongs in `parseTranscript` (session-scan.ts) as an `assistantLines` count,\n * which is free there since it already reads every line of the file. That file\n * was mid-edit in another session when this landed, so this stays out of it.\n * Move to the exact signal when it exists; the tests below should keep passing\n * unchanged, since they assert the outcome and not the proxy.\n */\nfunction winsTiebreak(candidate: UnifiedSession, incumbent: UnifiedSession): boolean {\n const c = candidate.diskSession;\n const i = incumbent.diskSession;\n\n // A live entry has no transcript to weigh. It also never reaches here except\n // against another live entry, where recency is all there is.\n if (c && i) {\n if (c.userLines !== i.userLines) return c.userLines > i.userLines;\n if (c.msgCount !== i.msgCount) return c.msgCount > i.msgCount;\n }\n\n return candidate.lastActivity > incumbent.lastActivity;\n}\n\nexport function buildDeduped(\n liveSessions: AiBrokerSessionMeta[],\n diskSessions: ScannedSession[],\n registeredProjects?: RegisteredProject[],\n showAll = false\n): UnifiedSession[] {\n const byName = new Map<string, UnifiedSession>();\n\n // Process live Claude sessions\n for (const s of liveSessions) {\n if (s.kind === \"shell\") continue;\n // NOT the iTerm tab title.\n //\n // `s.name` is whatever iTerm is calling the tab right now, which tracks the\n // foreground process and the window title escape codes. Using it as a\n // session IDENTITY means any tab that happens to be titled like a project\n // becomes a \"live session\" of that name — and because live entries win over\n // project entries in the merge below, `pai <Project>` then SWITCHES to that\n // tab instead of launching the project. Reported 2026-08-04: `pai PAI`\n // printing \"Switched to live session: pai\" and opening nothing.\n //\n // A session's identity is the name PAI gave it. A session that has none is\n // listed by its id and matches nothing by name, which is the safe answer:\n // failing to find it launches a fresh one, whereas matching it wrongly\n // hands the user someone else's terminal.\n const raw = s.paiName ?? s.sessionId.slice(0, 8);\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n const entry: UnifiedSession = {\n name: normalized,\n status: \"live\",\n liveSessionId: s.sessionId,\n lastActivity: Date.now(),\n project: \"\",\n lastPrompt: s.lastPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing || STATUS_PRIORITY[\"live\"] < STATUS_PRIORITY[existing.status]) {\n byName.set(key, entry);\n }\n }\n\n // Process disk sessions\n for (const s of diskSessions) {\n const raw = s.friendlyName ?? s.shortId;\n const normalized = normalizeName(raw);\n const key = normalized.toLowerCase();\n\n let status: UnifiedStatus;\n if (s.resumable) {\n status = \"resumable\";\n } else if (s.sessionStatus === \"transcript-only\") {\n status = \"transcript-only\";\n } else if (s.sessionStatus === \"stub\") {\n status = \"stub\";\n } else {\n status = \"orphan\";\n }\n\n const entry: UnifiedSession = {\n name: normalized,\n status,\n diskSession: s,\n lastActivity: s.mtime,\n project: s.decodedPath ?? \"\",\n lastPrompt: s.lastUserPrompt ?? \"\",\n };\n\n const existing = byName.get(key);\n if (!existing) {\n byName.set(key, entry);\n } else {\n const ep = STATUS_PRIORITY[existing.status];\n const np = STATUS_PRIORITY[status];\n if (np < ep || (np === ep && winsTiebreak(entry, existing))) {\n byName.set(key, entry);\n }\n }\n }\n\n // Constants for idle-project filtering\n const COLD_DAYS = 90;\n const COLD_THRESHOLD_MS = COLD_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // Process registered projects — add any not already covered by live/disk entries\n if (registeredProjects) {\n for (const p of registeredProjects) {\n // Skip archived projects unless showAll\n if (!showAll && p.status !== \"active\") continue;\n\n const displayName = p.display_name ?? p.slug;\n const normalized = normalizeName(displayName);\n const key = normalized.toLowerCase();\n\n // Also check if slug matches (via slug→words form)\n const slugKey = slugToWords(p.slug).toLowerCase();\n\n if (!byName.has(key) && !byName.has(slugKey)) {\n // Apply idle-project filter unless showAll:\n // - Hide projects with 0 sessions (never used)\n // - Hide projects cold AND low usage: last_active > 90 days AND sessions < 3\n if (!showAll) {\n if (p.session_count === 0) continue;\n if (p.session_count < 3) {\n const lastActive = p.last_active ?? 0;\n if (now - lastActive > COLD_THRESHOLD_MS) continue;\n }\n }\n\n byName.set(key, {\n name: normalized,\n slug: p.slug,\n status: \"project\",\n lastActivity: p.last_active ?? 0,\n project: p.root_path,\n lastPrompt: \"\",\n sessionCount: p.session_count,\n });\n } else {\n // Already have an entry — enrich it with the slug for matching\n const existing = byName.get(key) ?? byName.get(slugKey);\n if (existing && !existing.slug) {\n existing.slug = p.slug;\n }\n if (existing && existing.sessionCount === undefined) {\n existing.sessionCount = p.session_count;\n }\n }\n }\n }\n\n // Sort: by priority first, then by lastActivity desc\n const entries = Array.from(byName.values());\n entries.sort((a, b) => {\n const pa = STATUS_PRIORITY[a.status];\n const pb = STATUS_PRIORITY[b.status];\n if (pa !== pb) return pa - pb;\n return b.lastActivity - a.lastActivity;\n });\n\n return entries;\n}\n\n// ---------------------------------------------------------------------------\n// Status formatter\n// ---------------------------------------------------------------------------\n\nexport function fmtUnifiedStatus(s: UnifiedStatus): string {\n switch (s) {\n case \"live\": return chalk.green(\"live\");\n case \"resumable\": return chalk.cyan(\"resumable\");\n case \"transcript-only\": return chalk.dim(\"transcript\");\n case \"stub\": return chalk.dim(\"stub\");\n case \"project\": return chalk.dim(\"idle\");\n case \"orphan\": return chalk.dim(\"orphan\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared table renderer\n// ---------------------------------------------------------------------------\n\nfunction shortenProject(p: string, maxLen = 28): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n/**\n * Render the deduped session catalog to stdout.\n *\n * @param entries Output of buildDeduped()\n * @param maxRows Maximum rows to display. undefined = no limit (for --all).\n */\nexport function renderDedupedSessions(entries: UnifiedSession[], maxRows?: number): void {\n if (entries.length === 0) {\n console.log(warn(\"No sessions found. Start Claude Code in a project directory first.\"));\n return;\n }\n\n console.log(\"\\n\" + header(\"Sessions\") + \"\\n\");\n\n const visible = maxRows !== undefined ? entries.slice(0, maxRows) : entries;\n const tableHeaders = [\"#\", \"name\", \"status\", \"age\", \"project\", \"last prompt\"];\n const tableRows = visible.map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : entry.lastActivity > 0\n ? dim(fmtAge(entry.lastActivity))\n : dim(\"—\");\n const snippet = entry.lastPrompt.replace(/\\n+/g, \" \").trim().slice(0, 36);\n const projectPath =\n entry.diskSession\n ? entry.diskSession.friendlyName ?? entry.diskSession.decodedPath\n : entry.project;\n const project = dim(shortenProject(projectPath, 28));\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n chalk.dim(snippet ? `\"${snippet}\"` : \"—\"),\n ];\n });\n\n console.log(renderTable(tableHeaders, tableRows));\n\n if (maxRows !== undefined && entries.length > maxRows) {\n console.log(dim(` … ${entries.length - maxRows} more — use --all to show everything`));\n }\n\n console.log();\n console.log(\n dim(\" Switch/resume/start: \") +\n chalk.white(\"pai <name>\") +\n dim(\" or \") +\n chalk.white(\"pai <uuid-prefix>\")\n );\n console.log();\n}\n","/**\n * main-resolver.ts\n *\n * `pai [<query>]` — topic-first session discovery and launcher (v0.11.1)\n *\n * Decision tree:\n * 1. No arg → show deduped session listing (one row per name)\n * 2. UUID prefix → universal filesystem scan; auto-launch the match\n * 3. Any string:\n * a. Live match (by normalized paiName) → aibroker_switch → iTerm tab to front. Done.\n * b. Resumable match → probe + claude --resume <uuid>\n * c. Transcript/stub match → fresh claude in same project dir\n * d. No name match → free-text history search → picker\n *\n * Dedup + name normalization logic: src/cli/lib/dedup-sessions.ts (shared with listing).\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { err, dim, warn, ok, header, renderTable } from \"../utils.js\";\nimport {\n scanSessions,\n fmtAge,\n type ScannedSession,\n} from \"../lib/session-scan.js\";\nimport { searchHistory, HISTORY_FILE, type SessionMatch } from \"../lib/history-search.js\";\nimport { fetchLiveSessions, fetchLiveSessionsWithPrompts, switchToSession } from \"../lib/aibroker-client.js\";\nimport { basename } from \"node:path\";\nimport { launchInDir } from \"../lib/launch.js\";\nimport {\n buildDeduped,\n normalizeName,\n STATUS_PRIORITY,\n fmtUnifiedStatus,\n renderDedupedSessions,\n type UnifiedSession,\n type RegisteredProject,\n} from \"../lib/dedup-sessions.js\";\n\n// ---------------------------------------------------------------------------\n// Launch session (disk-based)\n// ---------------------------------------------------------------------------\n\n/**\n * Which directory should we open for this session?\n *\n * A session carries up to three ideas of where it lives, they disagree in\n * practice, and the FIRST is the least trustworthy. `clcDirectory` and\n * `decodedPath` are recorded per session and go stale the moment a project\n * directory is renamed; `registryRootPath` is kept current by the registry.\n *\n * This used to take the preferred value and, if it did not resolve, print an\n * error and exit — so a live project became unopenable by its own name. The\n * session had been started when its directory had a different name, so the\n * stale per-session value pointed at a path that no longer existed while the\n * registry held the right one, untried.\n *\n * The order is unchanged, because the per-session value IS more specific when\n * valid — it can name a subdirectory the session actually ran in. What changed\n * is that a candidate which fails no longer ends the attempt.\n *\n * realpath, not resolve: a directory reached through a symlinked parent has\n * several valid spellings, and comparing or storing the unresolved one is how\n * the same directory ends up with two identities.\n *\n * Returns the resolved directory plus every candidate tried, so a total failure\n * can report what it looked at instead of naming one path and leaving the user\n * to guess whether the others were even considered.\n */\nexport function resolveSessionDir(session: {\n clcDirectory?: string;\n registryRootPath?: string;\n decodedPath?: string;\n}): { dir: string | undefined; tried: string[] } {\n const tried = [\n session.clcDirectory,\n session.registryRootPath,\n session.decodedPath,\n ].filter((d): d is string => typeof d === \"string\" && d.length > 0);\n\n for (const candidate of tried) {\n try {\n return { dir: realpathSync(candidate), tried };\n } catch {\n // Stale, renamed or deleted — try the next one.\n }\n }\n return { dir: undefined, tried };\n}\n\nfunction launchSession(\n session: ScannedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): boolean {\n let resumableUuid: string | undefined;\n\n if (session.resumable) {\n resumableUuid = session.uuid;\n } else if (session.encodedDir) {\n const sameProject = allSessions.filter(\n (s) => s.encodedDir === session.encodedDir && s.resumable\n );\n sameProject.sort((a, b) => b.mtime - a.mtime);\n if (sameProject.length > 0) {\n resumableUuid = sameProject[0].uuid;\n }\n }\n\n const { dir: projectDir } = resolveSessionDir(session);\n\n // A dead session must not end the whole attempt.\n //\n // This used to print an error and process.exit(1). But the caller has more\n // candidates after this one — most importantly the registered PROJECT, whose\n // path the registry keeps current — and `openMatch` already knows how to open\n // a directory directly. Exiting here meant one stale transcript made a live\n // project unreachable by name: the user asks for a name they can see in the\n // listing, and gets a dead path instead of a session in the directory that\n // plainly exists.\n //\n // Returning false lets resolution continue. If nothing at all can be opened,\n // the caller reports that once, with everything it tried.\n if (projectDir === undefined) return false;\n\n // A session is named for a human to recognise, so a UUID prefix is never an\n // acceptable answer. `friendlyName` is undefined for the sessions synthesized\n // from a prompt-history match, and picking one of those used to open a tab\n // called \"7fdbb9a8\" — for a project plainly called Paperfull, sitting right\n // there in the path we already resolved. The directory is the better fallback\n // in every case, so shortId is now only reached for a path with no basename.\n const name = session.friendlyName ?? (basename(projectDir) || session.shortId);\n\n // Hand the launch to the shared router, never spawn claude here. This used to\n // spawn `claude` directly in every branch — resume, resume-fallback and fresh —\n // so `pai <name>` ran on Anthropic credentials even with a worker provider\n // active. launchInDir owns the routing: fresh launches follow the workers\n // config (provider when routing is on), a wanted resume stays claude because\n // its transcript is claude-format. Duplicating that dance here is also how\n // probeResume ended up fixed in one copy and not the others.\n launchInDir(projectDir, name, { resumableUuid, dryRun });\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// History match → ScannedSession bridge\n// ---------------------------------------------------------------------------\n\nfunction matchToSession(\n match: SessionMatch,\n allSessions: ScannedSession[]\n): ScannedSession | null {\n if (!match.sessionId) return null;\n\n const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);\n if (catalogMatch) return catalogMatch;\n\n if (!match.project) return null;\n\n return {\n uuid: match.sessionId,\n shortId: match.sessionId.slice(0, 8),\n encodedDir: \"\",\n decodedPath: match.project,\n topLevelPath: \"\",\n topLevelSystemLines: 0,\n topLevelSize: 0,\n resumable: false,\n sessionStatus: \"transcript-only\",\n sessionJsonlPath: undefined,\n userLines: 0,\n lastUserPrompt: match.lastMatchDisplay.slice(0, 80),\n msgCount: 0,\n mtime: match.lastMatchTs,\n friendlyName: undefined,\n clcDirectory: undefined,\n registryRootPath: match.project,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatters\n// ---------------------------------------------------------------------------\n\nfunction fmtTs(ts: number): string {\n const d = new Date(ts);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +\n `${pad(d.getHours())}:${pad(d.getMinutes())}`\n );\n}\n\nfunction shortenProject(p: string, maxLen = 44): string {\n if (!p || p.length <= maxLen) return p || dim(\"—\");\n return \"…\" + p.slice(-(maxLen - 1));\n}\n\n// ---------------------------------------------------------------------------\n// Interactive picker prompt\n// ---------------------------------------------------------------------------\n\nasync function askForChoice(max: number): Promise<number | null> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(\n dim(`\\n Enter # to launch (1-${max}), or press Enter to cancel: `),\n (answer) => {\n rl.close();\n const n = parseInt(answer.trim(), 10);\n if (!isNaN(n) && n >= 1 && n <= max) {\n resolve(n);\n } else {\n resolve(null);\n }\n }\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Shared switch helper (live session → iTerm tab)\n// ---------------------------------------------------------------------------\n\nasync function doSwitch(\n entry: UnifiedSession,\n dryRun: boolean\n): Promise<boolean> {\n if (!entry.liveSessionId) return false;\n if (dryRun) {\n console.log(\"\\n\" + chalk.bold(\"Dry run — would switch iTerm tab:\") + \"\\n\");\n console.log(` target: ${entry.name} (${entry.liveSessionId.slice(0, 8)})`);\n console.log(` action: aibroker_switch + osascript iTerm activate`);\n console.log();\n return true;\n }\n const result = await switchToSession(entry.liveSessionId);\n if (result.ok) {\n // Name AND id. \"Switched to live session: pai\" was reported three times on\n // 2026-08-04 by a user watching nothing happen, and the message gave no way\n // to tell WHICH session had been revealed — so there was nothing to check\n // against the tabs actually on screen. The id makes the claim falsifiable.\n console.log(\n ok(\n `Switched to live session: ${chalk.white(entry.name)} ` +\n chalk.dim(`(${entry.liveSessionId.slice(0, 8)})`),\n ),\n );\n return true;\n }\n console.error(warn(`Could not switch via AIBroker: ${result.error ?? \"unknown error\"}`));\n return false;\n}\n\n/**\n * Act on a matched catalog entry: switch to it, resume it, or start it.\n *\n * The third case is the one that was missing. A registered project whose\n * sessions have all ended matches by name perfectly well, but carries no\n * `diskSession`, and the old code simply ran off the end of both the exact and\n * the partial branch — past the picker, into a free-text search of prompt\n * history. So `pai Paperfull` answered a request to open a project with a list\n * of five old conversations that merely mentioned the word, while the project's\n * own directory was sitting in `entry.project` the whole time.\n *\n * Returns false only when there is genuinely nothing to open, which is the one\n * case where falling through to a history search is the right thing to do.\n */\nasync function openMatch(\n entry: UnifiedSession,\n allSessions: ScannedSession[],\n dryRun: boolean\n): Promise<boolean> {\n if (entry.status === \"live\") {\n if (await doSwitch(entry, dryRun)) return true;\n // Switch failed — fall through and try to open it from disk instead.\n }\n // A transcript whose directory is gone is not an answer. Fall through to the\n // registered project below, whose path the registry keeps current — that is\n // the difference between \"your project moved\" and \"cannot open anything\".\n if (entry.diskSession && launchSession(entry.diskSession, allSessions, dryRun)) {\n return true;\n }\n if (entry.project && existsSync(entry.project)) {\n launchInDir(entry.project, entry.name, { dryRun });\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Main command\n// ---------------------------------------------------------------------------\n\nexport interface MainResolverOpts {\n auto?: boolean; // -y / --auto: pick #1 without prompting\n dryRun?: boolean; // --dry-run: show what would happen\n n?: string; // --n <count>: max candidates for history search\n all?: boolean; // --all: show cold / 0-session / archived projects too\n}\n\nfunction getRegisteredProjects(db: Database, all = false): RegisteredProject[] {\n try {\n const statusClause = all ? \"\" : \"WHERE p.status = 'active'\";\n return db\n .prepare(`\n SELECT\n p.slug,\n p.display_name,\n p.root_path,\n p.status,\n COUNT(s.id) AS session_count,\n MAX(s.created_at) AS last_active\n FROM projects p\n LEFT JOIN sessions s ON s.project_id = p.id\n ${statusClause}\n GROUP BY p.id\n ORDER BY last_active DESC NULLS LAST, p.updated_at DESC\n `)\n .all() as RegisteredProject[];\n } catch {\n return [];\n }\n}\n\nexport async function cmdMain(\n db: Database,\n query: string | undefined,\n pickN: number | undefined,\n opts: MainResolverOpts\n): Promise<void> {\n const maxResults = parseInt(opts.n ?? \"20\", 10);\n const showAll = opts.all ?? false;\n // Live sessions: metadata-only fetch (1s) — last-prompt fetch is too slow\n // (~8s for AppleScript per-session scrollback). For live entries, last-prompt\n // stays empty; user can `pai <name>` to switch into the tab and see context.\n const livePromise = !query\n ? fetchLiveSessions().catch(() => [] as Awaited<ReturnType<typeof fetchLiveSessions>>)\n : Promise.resolve([]);\n const allSessions = scanSessions(db, { limit: 500, filter: \"named\" });\n const registeredProjects = getRegisteredProjects(db, showAll);\n\n // -----------------------------------------------------------------------\n // Case 1: No query → deduped session listing (shared renderer)\n // -----------------------------------------------------------------------\n if (!query) {\n const liveSessions = await livePromise;\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n renderDedupedSessions(deduped, showAll ? undefined : maxResults);\n return;\n }\n\n // -----------------------------------------------------------------------\n // Case 2: UUID prefix (8+ hex chars) → universal filesystem scan\n // -----------------------------------------------------------------------\n const UUID_PREFIX_RE = /^[0-9a-f-]{8,36}$/i;\n if (UUID_PREFIX_RE.test(query)) {\n const byUuid = allSessions.filter((s) => s.uuid.startsWith(query.toLowerCase()));\n if (byUuid.length === 1) {\n launchSession(byUuid[0], allSessions, opts.dryRun ?? false);\n return;\n }\n if (byUuid.length > 1) {\n console.error(err(`UUID prefix \"${query}\" is ambiguous — ${byUuid.length} catalog matches.`));\n process.exitCode = 1;\n return;\n }\n // Fall through to name match / history search\n }\n\n // -----------------------------------------------------------------------\n // Case 3: Name match against deduped catalog (normalized + slug matching)\n // -----------------------------------------------------------------------\n {\n let liveSessions: Awaited<ReturnType<typeof fetchLiveSessions>> = [];\n try {\n liveSessions = await fetchLiveSessions();\n } catch {\n // AIBroker not running\n }\n\n const deduped = buildDeduped(liveSessions, allSessions, registeredProjects, showAll);\n // Normalize the query the same way we normalize session names\n // Also support slug form: \"jobs-beta\" → \"jobs beta\" for matching\n const qNorm = normalizeName(query).toLowerCase();\n const qSlug = query.toLowerCase().replace(/\\s+/g, \"-\"); // words → slug form for slug lookup\n\n // Match helper: checks normalized display name AND slug\n const nameMatches = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase() === q ||\n (e.slug !== undefined && e.slug.toLowerCase() === qSlug);\n const nameIncludes = (e: UnifiedSession, q: string) =>\n e.name.toLowerCase().includes(q) ||\n (e.slug !== undefined && e.slug.toLowerCase().includes(qSlug));\n\n // Among equally-good matches, the most recently active one is what the user\n // means. This used `deduped.find()`, which took whichever entry happened to\n // come first — so a two-month-old note whose title matched EXACTLY beat the\n // live project the user was actually working in, whose sessions carried an\n // extra word in their names. Typing the project's own name opened its oldest\n // session.\n const newestFirst = (a: UnifiedSession, b: UnifiedSession) =>\n b.lastActivity - a.lastActivity;\n\n // Exact normalized-name match first (display_name or slug), newest wins.\n //\n // Try EVERY exact match, not just the best one. They are ranked by recency,\n // but the top-ranked entry can be unopenable — a transcript pointing at a\n // directory that has since been renamed — while a lower-ranked entry for the\n // same name is the live project sitting right there. Stopping at the first\n // candidate turned \"this name is ambiguous\" into \"this name is broken\".\n const exactMatches = deduped.filter((e) => nameMatches(e, qNorm)).sort(newestFirst);\n for (const match of exactMatches) {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n }\n\n // Partial normalized-name match (display_name or slug)\n let partialMatches = deduped.filter((e) => nameIncludes(e, qNorm));\n\n // Substring matching misses the common case where every query word is\n // present but not adjacent: \"foo baz\" is not a substring of \"foo bar baz\",\n // so a project whose directory gained a word became unreachable by the name\n // its owner thinks of it by. Fall back to requiring every query WORD to\n // appear somewhere in the name or slug. That stays narrow — a sibling\n // project sharing only the first word still will not match, because it\n // lacks the second.\n if (partialMatches.length === 0) {\n const qWords = qNorm.split(/\\s+/).filter(Boolean);\n if (qWords.length > 1) {\n partialMatches = deduped.filter((e) => {\n const haystack = `${e.name} ${e.slug ?? \"\"}`.toLowerCase();\n return qWords.every((w) => haystack.includes(w));\n });\n }\n }\n\n partialMatches.sort(newestFirst);\n\n if (partialMatches.length === 1) {\n if (await openMatch(partialMatches[0], allSessions, opts.dryRun ?? false)) return;\n }\n\n if (partialMatches.length > 1) {\n // Multiple matches — show picker\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"name\", \"status\", \"age\", \"project\"];\n const rows = partialMatches.slice(0, maxResults).map((entry, i) => {\n const age =\n entry.status === \"live\"\n ? chalk.green(\"now\")\n : dim(fmtAge(entry.lastActivity));\n const project = entry.diskSession\n ? dim(shortenProject(entry.diskSession.decodedPath, 36))\n : dim(\"—\");\n return [\n dim(String(i + 1)),\n chalk.white(entry.name),\n fmtUnifiedStatus(entry.status),\n age,\n project,\n ];\n });\n console.log(renderTable(headers, rows));\n console.log();\n\n const pickMatch = async (match: UnifiedSession) => {\n if (await openMatch(match, allSessions, opts.dryRun ?? false)) return;\n console.error(err(`Nothing to open for \"${match.name}\" — no live session, no transcript, no directory.`));\n process.exitCode = 1;\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < partialMatches.length) {\n await pickMatch(partialMatches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n await pickMatch(partialMatches[0]);\n return;\n }\n\n const choice = await askForChoice(Math.min(partialMatches.length, maxResults));\n if (choice !== null) {\n await pickMatch(partialMatches[choice - 1]);\n }\n return;\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 3b: Registered project by name → open its directory\n // -----------------------------------------------------------------------\n // If a project is registered under this name and its directory is there, that\n // is the answer, full stop. Naming a project should start a session in it.\n //\n // The deduped catalog cannot be relied on for this: it keeps ONE entry per\n // project and prefers a transcript over the registry row, so when the\n // transcript's recorded directory has gone stale the surviving entry carries a\n // dead path and the live registry value is never consulted. The catalog is\n // built for listing, where showing the session is right; it is the wrong\n // source for \"where do I open this\".\n //\n // So ask the registry directly, before falling back to scraping prompt\n // history — which is a search feature, not a way to open a known project.\n {\n const q = query.toLowerCase().replace(/[-_]+/g, \" \").replace(/\\s+/g, \" \").trim();\n const byName = registeredProjects.filter((p) => {\n const name = (p.display_name ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n const slug = (p.slug ?? \"\").toLowerCase().replace(/[-_]+/g, \" \").trim();\n return name === q || slug === q;\n });\n for (const p of byName) {\n if (p.root_path && existsSync(p.root_path)) {\n launchInDir(p.root_path, p.display_name ?? query, { dryRun: opts.dryRun ?? false });\n return;\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Case 4: Free-text history search\n // -----------------------------------------------------------------------\n if (!existsSync(HISTORY_FILE)) {\n console.error(err(\"~/.claude/history.jsonl not found.\"));\n console.error(dim(\" No prompt history available for search.\"));\n console.error(dim(\" Try: pai (no args) to see all sessions.\"));\n process.exitCode = 1;\n return;\n }\n\n process.stderr.write(dim(` Searching prompt history for \"${query}\"...\\n`));\n const matches = await searchHistory(query, maxResults);\n\n if (matches.length === 0) {\n console.log(warn(`No sessions found matching \"${query}\".`));\n console.log(dim(\" Try a shorter or different search term.\"));\n console.log(dim(\" Or run: \") + chalk.white(\"pai\") + dim(\" (no args) to see all sessions.\"));\n return;\n }\n\n console.log(\"\\n\" + header(`Sessions matching \"${query}\"`) + \"\\n\");\n const headers = [\"#\", \"id\", \"when\", \"project\", \"last matching prompt\"];\n const rows = matches.map((m, idx) => {\n const shortId = (m.sessionId ?? \"—\").slice(0, 8);\n const when = m.lastMatchTs > 0 ? fmtTs(m.lastMatchTs) : dim(\"—\");\n const project = shortenProject(m.project || \"—\");\n const snippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim().slice(0, 48);\n const fullSnippet = m.lastMatchDisplay.replace(/\\n+/g, \" \").trim();\n const display = snippet.length < fullSnippet.length ? `\"${snippet}…\"` : `\"${snippet}\"`;\n return [\n dim(String(idx + 1)),\n chalk.cyan(shortId),\n when,\n dim(project),\n chalk.dim(display),\n ];\n });\n\n console.log(renderTable(headers, rows));\n console.log();\n\n const launchHistoryMatch = (match: SessionMatch) => {\n const session = matchToSession(match, allSessions);\n if (!session) {\n console.error(err(\"Could not resolve session for launch (no project path).\"));\n process.exitCode = 1;\n return;\n }\n launchSession(session, allSessions, opts.dryRun ?? false);\n };\n\n if (pickN !== undefined) {\n const idx = pickN - 1;\n if (idx >= 0 && idx < matches.length) {\n launchHistoryMatch(matches[idx]);\n return;\n }\n console.error(err(`Invalid choice: ${pickN}`));\n process.exitCode = 1;\n return;\n }\n\n if (opts.auto) {\n launchHistoryMatch(matches[0]);\n return;\n }\n\n const choice = await askForChoice(matches.length);\n if (choice !== null) {\n launchHistoryMatch(matches[choice - 1]);\n }\n}\n"],"mappings":"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"}