@tekmidian/pai 0.5.6 → 0.6.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 (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +107 -3
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1897 -1569
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +12 -9
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
  18. package/dist/db-BtuN768f.mjs.map +1 -0
  19. package/dist/db-DdUperSl.mjs +110 -0
  20. package/dist/db-DdUperSl.mjs.map +1 -0
  21. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  22. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  23. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  24. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  25. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  26. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  27. package/dist/helpers-BEST-4Gx.mjs +420 -0
  28. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  29. package/dist/hooks/capture-all-events.mjs +19 -4
  30. package/dist/hooks/capture-all-events.mjs.map +4 -4
  31. package/dist/hooks/capture-session-summary.mjs +38 -0
  32. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  33. package/dist/hooks/cleanup-session-files.mjs +6 -12
  34. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  35. package/dist/hooks/context-compression-hook.mjs +105 -111
  36. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  37. package/dist/hooks/initialize-session.mjs +26 -17
  38. package/dist/hooks/initialize-session.mjs.map +4 -4
  39. package/dist/hooks/inject-observations.mjs +220 -0
  40. package/dist/hooks/inject-observations.mjs.map +7 -0
  41. package/dist/hooks/load-core-context.mjs +18 -2
  42. package/dist/hooks/load-core-context.mjs.map +4 -4
  43. package/dist/hooks/load-project-context.mjs +102 -97
  44. package/dist/hooks/load-project-context.mjs.map +4 -4
  45. package/dist/hooks/observe.mjs +354 -0
  46. package/dist/hooks/observe.mjs.map +7 -0
  47. package/dist/hooks/stop-hook.mjs +174 -90
  48. package/dist/hooks/stop-hook.mjs.map +4 -4
  49. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  50. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  51. package/dist/index.d.mts +32 -9
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +6 -9
  54. package/dist/indexer-D53l5d1U.mjs +1 -0
  55. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  56. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  57. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  58. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  59. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  60. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  61. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  62. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  63. package/dist/note-context-BK24bX8Y.mjs +126 -0
  64. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  65. package/dist/postgres-CKf-EDtS.mjs +846 -0
  66. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  67. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  68. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  69. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  70. package/dist/search-DC1qhkKn.mjs.map +1 -0
  71. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  72. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  73. package/dist/state-C6_vqz7w.mjs +102 -0
  74. package/dist/state-C6_vqz7w.mjs.map +1 -0
  75. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  76. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  77. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  78. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  79. package/dist/themes-BvYF0W8T.mjs +148 -0
  80. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  81. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  82. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  83. package/dist/trace-CRx9lPuc.mjs +137 -0
  84. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  85. package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
  86. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  87. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  88. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  89. package/package.json +1 -2
  90. package/src/hooks/ts/capture-all-events.ts +6 -0
  91. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  92. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  93. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  94. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  95. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  96. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  97. package/src/hooks/ts/lib/project-utils.ts +40 -999
  98. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  99. package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
  100. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  101. package/src/hooks/ts/session-start/initialize-session.ts +7 -1
  102. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  103. package/src/hooks/ts/session-start/load-core-context.ts +7 -0
  104. package/src/hooks/ts/session-start/load-project-context.ts +8 -1
  105. package/src/hooks/ts/stop/stop-hook.ts +28 -0
  106. package/templates/claude-md.template.md +7 -74
  107. package/templates/skills/user/.gitkeep +0 -0
  108. package/dist/chunker-CbnBe0s0.mjs +0 -191
  109. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  110. package/dist/config-Cf92lGX_.mjs.map +0 -1
  111. package/dist/daemon-D9evGlgR.mjs.map +0 -1
  112. package/dist/db-4lSqLFb8.mjs.map +0 -1
  113. package/dist/db-Dp8VXIMR.mjs +0 -212
  114. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  115. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  116. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  117. package/dist/mcp/index.d.mts +0 -1
  118. package/dist/mcp/index.mjs +0 -500
  119. package/dist/mcp/index.mjs.map +0 -1
  120. package/dist/postgres-FXrHDPcE.mjs +0 -358
  121. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  122. package/dist/schemas-BFIgGntb.mjs +0 -3405
  123. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  124. package/dist/search-_oHfguA5.mjs.map +0 -1
  125. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  126. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  127. package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
  128. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  129. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  130. package/templates/README.md +0 -181
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -1,11 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
- import { n as openRegistry } from "./db-4lSqLFb8.mjs";
3
- import { r as indexAll } from "./indexer-CMPOiY1r.mjs";
2
+ import { n as openRegistry } from "./db-BtuN768f.mjs";
3
+ import { d as sha256 } from "./helpers-BEST-4Gx.mjs";
4
+ import { n as indexAll } from "./sync-BOsnEj2-.mjs";
4
5
  import { t as configureEmbeddingModel } from "./embeddings-DGRAPAYb.mjs";
5
- import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-Cf92lGX_.mjs";
6
- import { a as toolProjectHealth, c as toolProjectTodo, d as toolSessionRoute, i as toolProjectDetect, l as toolRegistrySearch, n as toolMemorySearch, o as toolProjectInfo, s as toolProjectList, t as toolMemoryGet, u as toolSessionList } from "./tools-DV_lsiCc.mjs";
7
- import { t as createStorageBackend } from "./factory-Bzcy70G9.mjs";
8
- import { t as detectTopicShift } from "./detector-Bp-2SM3x.mjs";
6
+ import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-BuhHWyOK.mjs";
7
+ import { t as createStorageBackend } from "./factory-Ygqe_bVZ.mjs";
8
+ import { C as setStorageBackend, E as startTime, O as storageBackend, S as setStartTime, T as shutdownRequested, _ as setLastIndexTime, a as indexSchedulerTimer, b as setRegistryDb, c as lastVaultIndexTime, d as setDaemonConfig, f as setEmbedInProgress, g as setLastEmbedTime, h as setIndexSchedulerTimer, i as indexInProgress, k as vaultIndexInProgress, l as notificationConfig, m as setIndexInProgress, n as embedInProgress, o as lastEmbedTime, p as setEmbedSchedulerTimer, r as embedSchedulerTimer, s as lastIndexTime, t as daemonConfig, u as registryDb, v as setLastVaultIndexTime, w as setVaultIndexInProgress, x as setShutdownRequested, y as setNotificationConfig } from "./state-C6_vqz7w.mjs";
9
+ import { a as toolProjectDetect, c as toolProjectList, d as toolMemorySearch, i as toolSessionRoute, l as toolProjectTodo, n as toolRegistrySearch, o as toolProjectHealth, r as toolSessionList, s as toolProjectInfo, u as toolMemoryGet } from "./tools-DcaJlYDN.mjs";
10
+ import { t as detectTopicShift } from "./detector-jGBuYQJM.mjs";
9
11
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
12
  import { setPriority } from "node:os";
11
13
  import { randomUUID } from "node:crypto";
@@ -81,6 +83,182 @@ function patchNotificationConfig(patch) {
81
83
  return current;
82
84
  }
83
85
 
86
+ //#endregion
87
+ //#region src/daemon/daemon/scheduler.ts
88
+ /**
89
+ * Index, embed, and vault index schedulers for the PAI daemon.
90
+ * Exports run* functions (also called on-demand by the IPC handler)
91
+ * and the start* functions invoked once at daemon startup.
92
+ */
93
+ /** Minimum interval between vault index runs (30 minutes). */
94
+ const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
95
+ /**
96
+ * Run a full index pass. Guards against overlapping runs with indexInProgress.
97
+ * Called both by the scheduler and by the index_now IPC method.
98
+ */
99
+ async function runIndex() {
100
+ if (indexInProgress) {
101
+ process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
102
+ return;
103
+ }
104
+ if (embedInProgress) {
105
+ process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
106
+ return;
107
+ }
108
+ setIndexInProgress(true);
109
+ const t0 = Date.now();
110
+ try {
111
+ process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
112
+ if (storageBackend.backendType === "sqlite") {
113
+ const { SQLiteBackend } = await import("./sqlite-l-s9xPjY.mjs");
114
+ if (storageBackend instanceof SQLiteBackend) {
115
+ const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
116
+ const elapsed = Date.now() - t0;
117
+ setLastIndexTime(Date.now());
118
+ process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
119
+ }
120
+ } else {
121
+ const { indexAllWithBackend } = await import("./indexer-backend-jcJFsmB4.mjs");
122
+ const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
123
+ const elapsed = Date.now() - t0;
124
+ setLastIndexTime(Date.now());
125
+ process.stderr.write(`[pai-daemon] Index complete (postgres): ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
126
+ }
127
+ } catch (e) {
128
+ const msg = e instanceof Error ? e.message : String(e);
129
+ process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
130
+ } finally {
131
+ setIndexInProgress(false);
132
+ }
133
+ }
134
+ /**
135
+ * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
136
+ * Skips if no vaultPath is configured, or if project index/embed is in progress.
137
+ */
138
+ async function runVaultIndex() {
139
+ if (!daemonConfig.vaultPath) return;
140
+ if (vaultIndexInProgress) {
141
+ process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
142
+ return;
143
+ }
144
+ if (indexInProgress || embedInProgress) {
145
+ process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
146
+ return;
147
+ }
148
+ const { lastVaultIndexTime } = await import("./state-C6_vqz7w.mjs").then((n) => n.D);
149
+ if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
150
+ let vaultProjectId = daemonConfig.vaultProjectId;
151
+ if (!vaultProjectId) {
152
+ const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
153
+ vaultProjectId = row?.id ?? 999;
154
+ if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
155
+ }
156
+ setVaultIndexInProgress(true);
157
+ const t0 = Date.now();
158
+ process.stderr.write("[pai-daemon] Starting vault index run...\n");
159
+ try {
160
+ const { indexVault } = await import("./vault-indexer-Bi2cRmn7.mjs");
161
+ const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
162
+ const elapsed = Date.now() - t0;
163
+ setLastVaultIndexTime(Date.now());
164
+ process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
165
+ } catch (e) {
166
+ const msg = e instanceof Error ? e.message : String(e);
167
+ process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
168
+ } finally {
169
+ setVaultIndexInProgress(false);
170
+ }
171
+ }
172
+ /**
173
+ * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.
174
+ */
175
+ function startIndexScheduler() {
176
+ const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
177
+ process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
178
+ setTimeout(() => {
179
+ runIndex().then(() => runVaultIndex()).catch((e) => {
180
+ process.stderr.write(`[pai-daemon] Startup index error: ${e}\n`);
181
+ });
182
+ }, 2e3);
183
+ const timer = setInterval(() => {
184
+ runIndex().then(() => runVaultIndex()).catch((e) => {
185
+ process.stderr.write(`[pai-daemon] Scheduled index error: ${e}\n`);
186
+ });
187
+ }, intervalMs);
188
+ if (timer.unref) timer.unref();
189
+ setIndexSchedulerTimer(timer);
190
+ }
191
+ /**
192
+ * Run an embedding pass for all unembedded chunks (Postgres backend only).
193
+ */
194
+ async function runEmbed() {
195
+ if (embedInProgress) {
196
+ process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
197
+ return;
198
+ }
199
+ if (indexInProgress) {
200
+ process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
201
+ return;
202
+ }
203
+ if (storageBackend.backendType !== "postgres") return;
204
+ setEmbedInProgress(true);
205
+ const t0 = Date.now();
206
+ try {
207
+ process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
208
+ const projectNames = /* @__PURE__ */ new Map();
209
+ try {
210
+ const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
211
+ for (const r of rows) projectNames.set(r.id, r.slug);
212
+ } catch {}
213
+ const { embedChunksWithBackend } = await import("./indexer-backend-jcJFsmB4.mjs");
214
+ const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames);
215
+ let vaultEmbedCount = 0;
216
+ if (daemonConfig.vaultPath) try {
217
+ const { SQLiteBackend } = await import("./sqlite-l-s9xPjY.mjs");
218
+ const { openFederation } = await import("./db-DdUperSl.mjs").then((n) => n.t);
219
+ const federationDb = openFederation();
220
+ const vaultSqliteBackend = new SQLiteBackend(federationDb);
221
+ const vaultProjectNames = new Map(projectNames);
222
+ if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
223
+ vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames);
224
+ try {
225
+ federationDb.close();
226
+ } catch {}
227
+ if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
228
+ } catch (ve) {
229
+ const vmsg = ve instanceof Error ? ve.message : String(ve);
230
+ process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
231
+ }
232
+ const elapsed = Date.now() - t0;
233
+ setLastEmbedTime(Date.now());
234
+ process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
235
+ } catch (e) {
236
+ const msg = e instanceof Error ? e.message : String(e);
237
+ process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
238
+ } finally {
239
+ setEmbedInProgress(false);
240
+ }
241
+ }
242
+ /**
243
+ * Start the periodic embed scheduler. Initial run is 60 seconds after startup.
244
+ */
245
+ function startEmbedScheduler() {
246
+ const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
247
+ process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
248
+ setTimeout(() => {
249
+ runEmbed().catch((e) => {
250
+ process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
251
+ });
252
+ }, 6e4);
253
+ const timer = setInterval(() => {
254
+ runEmbed().catch((e) => {
255
+ process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
256
+ });
257
+ }, intervalMs);
258
+ if (timer.unref) timer.unref();
259
+ setEmbedSchedulerTimer(timer);
260
+ }
261
+
84
262
  //#endregion
85
263
  //#region src/notifications/providers/ntfy.ts
86
264
  var NtfyProvider = class {
@@ -302,223 +480,199 @@ async function routeNotification(payload, config) {
302
480
  }
303
481
 
304
482
  //#endregion
305
- //#region src/daemon/daemon.ts
483
+ //#region src/observations/store.ts
306
484
  /**
307
- * daemon.ts — The persistent PAI Daemon
308
- *
309
- * Provides shared database access, tool dispatch, and periodic index scheduling
310
- * for multiple concurrent Claude Code sessions via a Unix Domain Socket.
311
- *
312
- * Architecture:
313
- * MCP shims (Claude sessions) → Unix socket → PAI Daemon
314
- * ├── registry.db (shared, WAL, always SQLite)
315
- * ├── federation (SQLite or Postgres/pgvector)
316
- * ├── Embedding model (singleton)
317
- * └── Index scheduler (periodic)
318
- *
319
- * IPC protocol: NDJSON over Unix Domain Socket
320
- *
321
- * Request (shim → daemon):
322
- * { "id": "uuid", "method": "tool_name_or_special", "params": {} }
323
- *
324
- * Response (daemon → shim):
325
- * { "id": "uuid", "ok": true, "result": <any> }
326
- * { "id": "uuid", "ok": false, "error": "message" }
485
+ * store.ts — PostgreSQL persistence for PAI observations.
327
486
  *
328
- * Special methods:
329
- * status — Return daemon status (uptime, index state, db stats)
330
- * index_now — Trigger immediate index run (non-blocking)
487
+ * All functions accept a pg.Pool and are safe to call concurrently.
488
+ * Schema is initialized lazily via ensureObservationTables().
331
489
  *
332
- * All other methods are dispatched to the corresponding PAI tool function.
333
- *
334
- * Design notes:
335
- * - Registry stays in SQLite (small, simple metadata).
336
- * - Federation backend is configurable: SQLite (default) or Postgres/pgvector.
337
- * - Auto-fallback: if Postgres is configured but unavailable, falls back to SQLite.
338
- * - Index writes guarded by indexInProgress flag (not a mutex — index is idempotent).
339
- * - Embedding model loaded lazily on first semantic/hybrid request, then kept alive.
340
- * - Scheduler runs indexAll() every indexIntervalSecs (default 5 minutes).
490
+ * Content-hash deduplication: observations with the same hash
491
+ * created within a 30-second window are silently dropped to prevent
492
+ * duplicate entries from rapid repeated tool calls.
341
493
  */
342
- var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
343
- let registryDb;
344
- let storageBackend;
345
- let daemonConfig;
346
- let startTime = Date.now();
347
- let indexInProgress = false;
348
- let lastIndexTime = 0;
349
- let indexSchedulerTimer = null;
350
- let embedInProgress = false;
351
- let lastEmbedTime = 0;
352
- let embedSchedulerTimer = null;
353
- let vaultIndexInProgress = false;
354
- let lastVaultIndexTime = 0;
355
- /** Mutable notification config — loaded from disk at startup, patchable at runtime */
356
- let notificationConfig;
494
+ let _tablesEnsured = false;
357
495
  /**
358
- * Set to true when a SIGTERM/SIGINT is received so that long-running loops
359
- * (embed, index) can detect the signal and exit their inner loops before the
360
- * pool/backend is closed. Checked by embedChunksWithBackend() via the
361
- * `shouldStop` callback passed from runEmbed().
496
+ * Inlined schema DDL avoids runtime file reads that break in bundled code
497
+ * (the bundler puts this in a shared chunk whose __dirname differs from src/).
362
498
  */
363
- let shutdownRequested = false;
499
+ const SCHEMA_SQL = `
500
+ CREATE TABLE IF NOT EXISTS pai_observations (
501
+ id SERIAL PRIMARY KEY,
502
+ session_id TEXT NOT NULL,
503
+ project_id INTEGER,
504
+ project_slug TEXT,
505
+ type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
506
+ title TEXT NOT NULL,
507
+ narrative TEXT,
508
+ tool_name TEXT,
509
+ tool_input_summary TEXT,
510
+ files_read JSONB DEFAULT '[]'::jsonb,
511
+ files_modified JSONB DEFAULT '[]'::jsonb,
512
+ concepts JSONB DEFAULT '[]'::jsonb,
513
+ content_hash TEXT,
514
+ created_at TIMESTAMPTZ DEFAULT NOW()
515
+ );
516
+
517
+ CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
518
+ CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
519
+ CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
520
+ CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
521
+ CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
522
+
523
+ CREATE TABLE IF NOT EXISTS pai_session_summaries (
524
+ id SERIAL PRIMARY KEY,
525
+ session_id TEXT NOT NULL UNIQUE,
526
+ project_id INTEGER,
527
+ project_slug TEXT,
528
+ request TEXT,
529
+ investigated TEXT,
530
+ learned TEXT,
531
+ completed TEXT,
532
+ next_steps TEXT,
533
+ observation_count INTEGER DEFAULT 0,
534
+ created_at TIMESTAMPTZ DEFAULT NOW()
535
+ );
536
+
537
+ CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
538
+ CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
539
+ `;
364
540
  /**
365
- * Run a full index pass. Guards against overlapping runs with indexInProgress.
366
- * Called both by the scheduler and by the index_now IPC method.
367
- *
368
- * NOTE: We pass the raw SQLite federation DB to indexAll() for SQLite backend,
369
- * or skip and use the backend interface for Postgres. The indexer currently
370
- * uses better-sqlite3 directly; it will be refactored in a future phase.
371
- * For now, we keep the SQLite indexer path and add a Postgres-aware path.
541
+ * Run schema DDL idempotently against the given pool.
542
+ * Uses a module-level flag so subsequent calls are no-ops within the same
543
+ * process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
372
544
  */
373
- async function runIndex() {
374
- if (indexInProgress) {
375
- process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
376
- return;
377
- }
378
- if (embedInProgress) {
379
- process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
380
- return;
381
- }
382
- indexInProgress = true;
383
- const t0 = Date.now();
384
- try {
385
- process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
386
- if (storageBackend.backendType === "sqlite") {
387
- const { SQLiteBackend } = await import("./sqlite-WWBq7_2C.mjs");
388
- if (storageBackend instanceof SQLiteBackend) {
389
- const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
390
- const elapsed = Date.now() - t0;
391
- lastIndexTime = Date.now();
392
- process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
393
- }
394
- } else {
395
- const { indexAllWithBackend } = await import("./indexer-backend-CIMXedqk.mjs");
396
- const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
397
- const elapsed = Date.now() - t0;
398
- lastIndexTime = Date.now();
399
- process.stderr.write(`[pai-daemon] Index complete (postgres): ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
400
- }
401
- } catch (e) {
402
- const msg = e instanceof Error ? e.message : String(e);
403
- process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
404
- } finally {
405
- indexInProgress = false;
406
- }
545
+ async function ensureObservationTables(pool) {
546
+ if (_tablesEnsured) return;
547
+ await pool.query(SCHEMA_SQL);
548
+ _tablesEnsured = true;
407
549
  }
408
550
  /**
409
- * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
410
- * Skips if no vaultPath is configured, or if project index/embed is in progress.
411
- * Called both by the scheduler (chained after runIndex) and by the vault_index_now IPC method.
551
+ * Compute a 16-character hex content hash for deduplication.
552
+ * Hash = SHA256(session_id + tool_name + title).slice(0, 16)
412
553
  */
413
- async function runVaultIndex() {
414
- if (!daemonConfig.vaultPath) return;
415
- if (vaultIndexInProgress) {
416
- process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
417
- return;
418
- }
419
- if (indexInProgress || embedInProgress) {
420
- process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
421
- return;
422
- }
423
- vaultIndexInProgress = true;
424
- const t0 = Date.now();
425
- try {
426
- process.stderr.write("[pai-daemon] Starting vault index run...\n");
427
- if (storageBackend.backendType === "sqlite") {
428
- const { SQLiteBackend } = await import("./sqlite-WWBq7_2C.mjs");
429
- if (storageBackend instanceof SQLiteBackend) {
430
- const db = storageBackend.getRawDb();
431
- let vaultProjectId = daemonConfig.vaultProjectId;
432
- if (!vaultProjectId) vaultProjectId = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath)?.id ?? 0;
433
- if (!vaultProjectId) {
434
- process.stderr.write("[pai-daemon] Vault project ID not found. Register the vault as a project first.\n");
435
- return;
436
- }
437
- const { indexVault } = await import("./vault-indexer-DXWs9pDn.mjs");
438
- const result = await indexVault(db, vaultProjectId, daemonConfig.vaultPath);
439
- const elapsed = Date.now() - t0;
440
- lastVaultIndexTime = Date.now();
441
- process.stderr.write(`[pai-daemon] Vault index complete: ${result.filesIndexed} files, ${result.linksExtracted} links, ${result.deadLinksFound} dead, ${result.orphansFound} orphans (${elapsed}ms)\n`);
442
- }
443
- } else process.stderr.write("[pai-daemon] Vault indexing only supported on SQLite backend.\n");
444
- } catch (e) {
445
- const msg = e instanceof Error ? e.message : String(e);
446
- process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
447
- } finally {
448
- vaultIndexInProgress = false;
449
- }
554
+ function computeContentHash(sessionId, toolName, title) {
555
+ return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
450
556
  }
451
557
  /**
452
- * Start the periodic index scheduler.
558
+ * Insert an observation, skipping duplicates within a 30-second window.
559
+ * Returns the inserted row's id, or null if the insert was suppressed.
453
560
  */
454
- function startIndexScheduler() {
455
- const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
456
- process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
457
- setTimeout(() => {
458
- runIndex().then(() => runVaultIndex()).catch((e) => {
459
- process.stderr.write(`[pai-daemon] Startup index error: ${e}\n`);
460
- });
461
- }, 2e3);
462
- indexSchedulerTimer = setInterval(() => {
463
- runIndex().then(() => runVaultIndex()).catch((e) => {
464
- process.stderr.write(`[pai-daemon] Scheduled index error: ${e}\n`);
465
- });
466
- }, intervalMs);
467
- if (indexSchedulerTimer.unref) indexSchedulerTimer.unref();
561
+ async function storeObservation(pool, obs) {
562
+ await ensureObservationTables(pool);
563
+ const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
564
+ const dupCheck = await pool.query(`SELECT id FROM pai_observations
565
+ WHERE content_hash = $1
566
+ AND session_id = $2
567
+ AND created_at >= NOW() - INTERVAL '30 seconds'
568
+ LIMIT 1`, [hash, obs.session_id]);
569
+ if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
570
+ return (await pool.query(`INSERT INTO pai_observations
571
+ (session_id, project_id, project_slug, type, title, narrative,
572
+ tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
573
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
574
+ RETURNING id`, [
575
+ obs.session_id,
576
+ obs.project_id ?? null,
577
+ obs.project_slug ?? null,
578
+ obs.type,
579
+ obs.title,
580
+ obs.narrative ?? null,
581
+ obs.tool_name,
582
+ obs.tool_input_summary ?? null,
583
+ JSON.stringify(obs.files_read),
584
+ JSON.stringify(obs.files_modified),
585
+ JSON.stringify(obs.concepts),
586
+ hash
587
+ ])).rows[0]?.id ?? null;
468
588
  }
469
589
  /**
470
- * Run an embedding pass for all unembedded chunks (Postgres backend only).
471
- * Guards against overlapping runs with embedInProgress.
472
- * Skips if an index run is currently in progress to avoid contention.
590
+ * Filtered query for observations with optional projectId, sessionId, type,
591
+ * limit, and offset. Returns results ordered by created_at DESC.
473
592
  */
474
- async function runEmbed() {
475
- if (embedInProgress) {
476
- process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
477
- return;
593
+ async function queryObservations(pool, opts = {}) {
594
+ await ensureObservationTables(pool);
595
+ const conditions = [];
596
+ const params = [];
597
+ let idx = 1;
598
+ if (opts.projectId !== void 0) {
599
+ conditions.push(`project_id = $${idx++}`);
600
+ params.push(opts.projectId);
478
601
  }
479
- if (indexInProgress) {
480
- process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
481
- return;
602
+ if (opts.sessionId !== void 0) {
603
+ conditions.push(`session_id = $${idx++}`);
604
+ params.push(opts.sessionId);
482
605
  }
483
- if (storageBackend.backendType !== "postgres") return;
484
- embedInProgress = true;
485
- const t0 = Date.now();
486
- try {
487
- process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
488
- const { embedChunksWithBackend } = await import("./indexer-backend-CIMXedqk.mjs");
489
- const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested);
490
- const elapsed = Date.now() - t0;
491
- lastEmbedTime = Date.now();
492
- process.stderr.write(`[pai-daemon] Embed pass complete: ${count} chunks embedded (${elapsed}ms)\n`);
493
- } catch (e) {
494
- const msg = e instanceof Error ? e.message : String(e);
495
- process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
496
- } finally {
497
- embedInProgress = false;
606
+ if (opts.type !== void 0) {
607
+ conditions.push(`type = $${idx++}`);
608
+ params.push(opts.type);
498
609
  }
610
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
611
+ const limit = opts.limit ?? 50;
612
+ const offset = opts.offset ?? 0;
613
+ params.push(limit, offset);
614
+ return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
615
+ tool_name, tool_input_summary,
616
+ files_read, files_modified, concepts,
617
+ content_hash, created_at
618
+ FROM pai_observations
619
+ ${where}
620
+ ORDER BY created_at DESC
621
+ LIMIT $${idx++} OFFSET $${idx}`, params)).rows;
499
622
  }
500
623
  /**
501
- * Start the periodic embed scheduler.
502
- * Initial run is 30 seconds after startup (after the 2-second index startup run).
624
+ * Most recent observations for a project, ordered by created_at DESC.
503
625
  */
504
- function startEmbedScheduler() {
505
- const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
506
- process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
507
- setTimeout(() => {
508
- runEmbed().catch((e) => {
509
- process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
510
- });
511
- }, 3e4);
512
- embedSchedulerTimer = setInterval(() => {
513
- runEmbed().catch((e) => {
514
- process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
515
- });
516
- }, intervalMs);
517
- if (embedSchedulerTimer.unref) embedSchedulerTimer.unref();
626
+ async function queryRecentObservations(pool, projectId, limit) {
627
+ await ensureObservationTables(pool);
628
+ return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
629
+ tool_name, tool_input_summary,
630
+ files_read, files_modified, concepts,
631
+ content_hash, created_at
632
+ FROM pai_observations
633
+ WHERE project_id = $1
634
+ ORDER BY created_at DESC
635
+ LIMIT $2`, [projectId, limit])).rows;
518
636
  }
519
637
  /**
638
+ * Upsert a session summary. Uses ON CONFLICT on session_id so calling this
639
+ * multiple times with updated content is safe.
640
+ */
641
+ async function storeSessionSummary(pool, summary) {
642
+ await ensureObservationTables(pool);
643
+ await pool.query(`INSERT INTO pai_session_summaries
644
+ (session_id, project_id, project_slug, request, investigated,
645
+ learned, completed, next_steps, observation_count)
646
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
647
+ ON CONFLICT (session_id) DO UPDATE SET
648
+ project_id = EXCLUDED.project_id,
649
+ project_slug = EXCLUDED.project_slug,
650
+ request = EXCLUDED.request,
651
+ investigated = EXCLUDED.investigated,
652
+ learned = EXCLUDED.learned,
653
+ completed = EXCLUDED.completed,
654
+ next_steps = EXCLUDED.next_steps,
655
+ observation_count = EXCLUDED.observation_count`, [
656
+ summary.session_id,
657
+ summary.project_id ?? null,
658
+ summary.project_slug ?? null,
659
+ summary.request ?? null,
660
+ summary.investigated ?? null,
661
+ summary.learned ?? null,
662
+ summary.completed ?? null,
663
+ summary.next_steps ?? null,
664
+ summary.observation_count ?? 0
665
+ ]);
666
+ }
667
+
668
+ //#endregion
669
+ //#region src/daemon/daemon/dispatcher.ts
670
+ /**
671
+ * Tool dispatcher — maps IPC method names to PAI tool functions.
672
+ */
673
+ /**
520
674
  * Dispatch an IPC tool call to the appropriate tool function.
521
- * Returns the tool result or throws.
675
+ * Returns the tool result or throws on unknown/failed methods.
522
676
  */
523
677
  async function dispatchTool(method, params) {
524
678
  const p = params;
@@ -540,24 +694,48 @@ async function dispatchTool(method, params) {
540
694
  case "zettel_suggest":
541
695
  case "zettel_converse":
542
696
  case "zettel_themes": {
543
- const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes } = await import("./tools-DV_lsiCc.mjs").then((n) => n.y);
544
- if (storageBackend.backendType !== "sqlite") throw new Error("Zettel tools require SQLite backend");
545
- const { SQLiteBackend } = await import("./sqlite-WWBq7_2C.mjs");
546
- if (!(storageBackend instanceof SQLiteBackend)) throw new Error("Zettel tools require SQLite backend");
547
- const fedDb = storageBackend.getRawDb();
697
+ const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes } = await import("./tools-DcaJlYDN.mjs").then((n) => n.t);
548
698
  switch (method) {
549
- case "zettel_explore": return toolZettelExplore(fedDb, p);
550
- case "zettel_health": return toolZettelHealth(fedDb, p);
551
- case "zettel_surprise": return toolZettelSurprise(fedDb, p);
552
- case "zettel_suggest": return toolZettelSuggest(fedDb, p);
553
- case "zettel_converse": return toolZettelConverse(fedDb, p);
554
- case "zettel_themes": return toolZettelThemes(fedDb, p);
699
+ case "zettel_explore": return toolZettelExplore(storageBackend, p);
700
+ case "zettel_health": return toolZettelHealth(storageBackend, p);
701
+ case "zettel_surprise": return toolZettelSurprise(storageBackend, p);
702
+ case "zettel_suggest": return toolZettelSuggest(storageBackend, p);
703
+ case "zettel_converse": return toolZettelConverse(storageBackend, p);
704
+ case "zettel_themes": return toolZettelThemes(storageBackend, p);
555
705
  }
556
706
  break;
557
707
  }
708
+ case "graph_clusters": {
709
+ const { handleGraphClusters } = await import("./clusters-JIDQW65f.mjs");
710
+ return handleGraphClusters(storageBackend.getPool?.() ?? null, storageBackend, p);
711
+ }
712
+ case "graph_neighborhood": {
713
+ const { handleGraphNeighborhood } = await import("./neighborhood-BYYbEkUJ.mjs");
714
+ return handleGraphNeighborhood(storageBackend.getPool?.() ?? null, storageBackend, p);
715
+ }
716
+ case "graph_note_context": {
717
+ const { handleGraphNoteContext } = await import("./note-context-BK24bX8Y.mjs");
718
+ return handleGraphNoteContext(storageBackend.getPool?.() ?? null, storageBackend, p);
719
+ }
720
+ case "graph_trace": {
721
+ const { handleGraphTrace } = await import("./trace-CRx9lPuc.mjs");
722
+ return handleGraphTrace(storageBackend, p);
723
+ }
724
+ case "graph_latent_ideas": {
725
+ const { handleGraphLatentIdeas } = await import("./latent-ideas-bTJo6Omd.mjs");
726
+ return handleGraphLatentIdeas(storageBackend, p);
727
+ }
728
+ case "idea_materialize": {
729
+ const { handleIdeaMaterialize } = await import("./latent-ideas-bTJo6Omd.mjs");
730
+ if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
731
+ return handleIdeaMaterialize(p, daemonConfig.vaultPath);
732
+ }
558
733
  default: throw new Error(`Unknown method: ${method}`);
559
734
  }
560
735
  }
736
+
737
+ //#endregion
738
+ //#region src/daemon/daemon/handler.ts
561
739
  function sendResponse(socket, response) {
562
740
  try {
563
741
  socket.write(JSON.stringify(response) + "\n");
@@ -643,15 +821,16 @@ async function handleRequest(request, socket) {
643
821
  if (method === "notification_set_config") {
644
822
  try {
645
823
  const p = params;
646
- notificationConfig = patchNotificationConfig({
824
+ const updated = patchNotificationConfig({
647
825
  mode: p.mode,
648
826
  channels: p.channels,
649
827
  routing: p.routing
650
828
  });
829
+ setNotificationConfig(updated);
651
830
  sendResponse(socket, {
652
831
  id,
653
832
  ok: true,
654
- result: { config: notificationConfig }
833
+ result: { config: updated }
655
834
  });
656
835
  } catch (e) {
657
836
  sendResponse(socket, {
@@ -695,6 +874,266 @@ async function handleRequest(request, socket) {
695
874
  });
696
875
  return;
697
876
  }
877
+ if (method === "observation_store") {
878
+ const pool = storageBackend.getPool?.();
879
+ if (!pool) {
880
+ sendResponse(socket, {
881
+ id,
882
+ ok: false,
883
+ error: "Observations require Postgres backend"
884
+ });
885
+ socket.end();
886
+ return;
887
+ }
888
+ try {
889
+ const p = params;
890
+ let project_id = null;
891
+ let project_slug = null;
892
+ if (p.cwd) {
893
+ const row = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1").get(p.cwd);
894
+ if (row) {
895
+ project_id = row.id;
896
+ project_slug = row.slug;
897
+ }
898
+ }
899
+ await ensureObservationTables(pool);
900
+ sendResponse(socket, {
901
+ id,
902
+ ok: true,
903
+ result: {
904
+ ok: true,
905
+ id: await storeObservation(pool, {
906
+ session_id: p.session_id,
907
+ project_id,
908
+ project_slug,
909
+ type: p.type,
910
+ title: p.title,
911
+ narrative: p.narrative ?? null,
912
+ tool_name: p.tool_name,
913
+ tool_input_summary: p.tool_input_summary ?? null,
914
+ files_read: p.files_read ?? [],
915
+ files_modified: p.files_modified ?? [],
916
+ concepts: p.concepts ?? []
917
+ })
918
+ }
919
+ });
920
+ } catch (e) {
921
+ sendResponse(socket, {
922
+ id,
923
+ ok: false,
924
+ error: e instanceof Error ? e.message : String(e)
925
+ });
926
+ }
927
+ socket.end();
928
+ return;
929
+ }
930
+ if (method === "observation_query") {
931
+ const pool = storageBackend.getPool?.();
932
+ if (!pool) {
933
+ sendResponse(socket, {
934
+ id,
935
+ ok: false,
936
+ error: "Observations require Postgres backend"
937
+ });
938
+ socket.end();
939
+ return;
940
+ }
941
+ try {
942
+ const p = params;
943
+ sendResponse(socket, {
944
+ id,
945
+ ok: true,
946
+ result: await queryObservations(pool, {
947
+ projectId: p.project_id,
948
+ sessionId: p.session_id,
949
+ type: p.type,
950
+ limit: p.limit,
951
+ offset: p.offset
952
+ })
953
+ });
954
+ } catch (e) {
955
+ sendResponse(socket, {
956
+ id,
957
+ ok: false,
958
+ error: e instanceof Error ? e.message : String(e)
959
+ });
960
+ }
961
+ socket.end();
962
+ return;
963
+ }
964
+ if (method === "observation_recent") {
965
+ const pool = storageBackend.getPool?.();
966
+ if (!pool) {
967
+ sendResponse(socket, {
968
+ id,
969
+ ok: false,
970
+ error: "Observations require Postgres backend"
971
+ });
972
+ socket.end();
973
+ return;
974
+ }
975
+ try {
976
+ const p = params;
977
+ const limit = p.limit ?? 20;
978
+ let resolvedProjectId = p.project_id;
979
+ let resolvedProjectSlug;
980
+ if (resolvedProjectId === void 0 && p.cwd) {
981
+ const row = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1").get(p.cwd);
982
+ if (row) {
983
+ resolvedProjectId = row.id;
984
+ resolvedProjectSlug = row.slug;
985
+ }
986
+ }
987
+ let rows;
988
+ if (resolvedProjectId !== void 0) rows = await queryRecentObservations(pool, resolvedProjectId, limit);
989
+ else rows = await queryObservations(pool, { limit });
990
+ sendResponse(socket, {
991
+ id,
992
+ ok: true,
993
+ result: {
994
+ rows,
995
+ project_slug: resolvedProjectSlug
996
+ }
997
+ });
998
+ } catch (e) {
999
+ sendResponse(socket, {
1000
+ id,
1001
+ ok: false,
1002
+ error: e instanceof Error ? e.message : String(e)
1003
+ });
1004
+ }
1005
+ socket.end();
1006
+ return;
1007
+ }
1008
+ if (method === "observation_list") {
1009
+ const pool = storageBackend.getPool?.();
1010
+ if (!pool) {
1011
+ sendResponse(socket, {
1012
+ id,
1013
+ ok: false,
1014
+ error: "Observations require Postgres backend"
1015
+ });
1016
+ socket.end();
1017
+ return;
1018
+ }
1019
+ try {
1020
+ const p = params;
1021
+ let projectId;
1022
+ if (p.project_slug) projectId = registryDb.prepare("SELECT id FROM projects WHERE slug = ?").get(p.project_slug)?.id;
1023
+ sendResponse(socket, {
1024
+ id,
1025
+ ok: true,
1026
+ result: await queryObservations(pool, {
1027
+ projectId,
1028
+ sessionId: p.session_id,
1029
+ type: p.type,
1030
+ limit: p.limit ?? 20,
1031
+ offset: p.offset ?? 0
1032
+ })
1033
+ });
1034
+ } catch (e) {
1035
+ sendResponse(socket, {
1036
+ id,
1037
+ ok: false,
1038
+ error: e instanceof Error ? e.message : String(e)
1039
+ });
1040
+ }
1041
+ socket.end();
1042
+ return;
1043
+ }
1044
+ if (method === "observation_stats") {
1045
+ const pool = storageBackend.getPool?.();
1046
+ if (!pool) {
1047
+ sendResponse(socket, {
1048
+ id,
1049
+ ok: false,
1050
+ error: "Observations require Postgres backend"
1051
+ });
1052
+ socket.end();
1053
+ return;
1054
+ }
1055
+ try {
1056
+ await ensureObservationTables(pool);
1057
+ const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([
1058
+ pool.query("SELECT COUNT(*) as count FROM pai_observations"),
1059
+ pool.query("SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC"),
1060
+ pool.query("SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15"),
1061
+ pool.query("SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1")
1062
+ ]);
1063
+ sendResponse(socket, {
1064
+ id,
1065
+ ok: true,
1066
+ result: {
1067
+ total: parseInt(totalRes.rows[0]?.count ?? "0", 10),
1068
+ by_type: byTypeRes.rows.map((r) => ({
1069
+ type: r.type,
1070
+ count: parseInt(r.count, 10)
1071
+ })),
1072
+ by_project: byProjectRes.rows.map((r) => ({
1073
+ project_slug: r.project_slug,
1074
+ count: parseInt(r.count, 10)
1075
+ })),
1076
+ most_recent: recentRes.rows[0]?.created_at ?? null
1077
+ }
1078
+ });
1079
+ } catch (e) {
1080
+ sendResponse(socket, {
1081
+ id,
1082
+ ok: false,
1083
+ error: e instanceof Error ? e.message : String(e)
1084
+ });
1085
+ }
1086
+ socket.end();
1087
+ return;
1088
+ }
1089
+ if (method === "session_summary_store") {
1090
+ const pool = storageBackend.getPool?.();
1091
+ if (!pool) {
1092
+ sendResponse(socket, {
1093
+ id,
1094
+ ok: false,
1095
+ error: "Session summaries require Postgres backend"
1096
+ });
1097
+ socket.end();
1098
+ return;
1099
+ }
1100
+ try {
1101
+ const p = params;
1102
+ let resolvedProjectId = p.project_id ?? null;
1103
+ let resolvedProjectSlug = p.project_slug ?? null;
1104
+ if (resolvedProjectId === null && p.cwd) {
1105
+ const row = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1").get(p.cwd);
1106
+ if (row) {
1107
+ resolvedProjectId = row.id;
1108
+ resolvedProjectSlug = row.slug;
1109
+ }
1110
+ }
1111
+ await storeSessionSummary(pool, {
1112
+ session_id: p.session_id,
1113
+ project_id: resolvedProjectId,
1114
+ project_slug: resolvedProjectSlug,
1115
+ request: p.request ?? null,
1116
+ investigated: p.investigated ?? null,
1117
+ learned: p.learned ?? null,
1118
+ completed: p.completed ?? null,
1119
+ next_steps: p.next_steps ?? null,
1120
+ observation_count: p.observation_count ?? 0
1121
+ });
1122
+ sendResponse(socket, {
1123
+ id,
1124
+ ok: true,
1125
+ result: { ok: true }
1126
+ });
1127
+ } catch (e) {
1128
+ sendResponse(socket, {
1129
+ id,
1130
+ ok: false,
1131
+ error: e instanceof Error ? e.message : String(e)
1132
+ });
1133
+ }
1134
+ socket.end();
1135
+ return;
1136
+ }
698
1137
  try {
699
1138
  sendResponse(socket, {
700
1139
  id,
@@ -710,9 +1149,15 @@ async function handleRequest(request, socket) {
710
1149
  }
711
1150
  socket.end();
712
1151
  }
1152
+
1153
+ //#endregion
1154
+ //#region src/daemon/daemon/server.ts
1155
+ /**
1156
+ * IPC server and daemon entry point.
1157
+ * Owns: isSocketLive, startIpcServer, serve (exported).
1158
+ */
713
1159
  /**
714
1160
  * Check whether an existing socket file is actually being served by a live process.
715
- * Returns true if a daemon is already accepting connections, false otherwise.
716
1161
  */
717
1162
  function isSocketLive(path) {
718
1163
  return new Promise((resolve) => {
@@ -786,19 +1231,20 @@ async function startIpcServer(socketPath) {
786
1231
  return server;
787
1232
  }
788
1233
  async function serve(config) {
789
- daemonConfig = config;
790
- startTime = Date.now();
791
- notificationConfig = loadNotificationConfig();
1234
+ setDaemonConfig(config);
1235
+ setStartTime(Date.now());
1236
+ setNotificationConfig(loadNotificationConfig());
792
1237
  process.stderr.write("[pai-daemon] Starting daemon...\n");
793
1238
  process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
794
1239
  process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
1240
+ const { notificationConfig } = await import("./state-C6_vqz7w.mjs").then((n) => n.D);
795
1241
  process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
796
1242
  try {
797
1243
  setPriority(process.pid, 10);
798
1244
  } catch {}
799
1245
  configureEmbeddingModel(config.embeddingModel);
800
1246
  try {
801
- registryDb = openRegistry();
1247
+ setRegistryDb(openRegistry());
802
1248
  process.stderr.write("[pai-daemon] Registry database opened.\n");
803
1249
  } catch (e) {
804
1250
  const msg = e instanceof Error ? e.message : String(e);
@@ -806,8 +1252,9 @@ async function serve(config) {
806
1252
  process.exit(1);
807
1253
  }
808
1254
  try {
809
- storageBackend = await createStorageBackend(config);
810
- process.stderr.write(`[pai-daemon] Federation backend: ${storageBackend.backendType}\n`);
1255
+ const backend = await createStorageBackend(config);
1256
+ setStorageBackend(backend);
1257
+ process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
811
1258
  } catch (e) {
812
1259
  const msg = e instanceof Error ? e.message : String(e);
813
1260
  process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
@@ -819,7 +1266,7 @@ async function serve(config) {
819
1266
  const server = await startIpcServer(config.socketPath);
820
1267
  const shutdown = async (signal) => {
821
1268
  process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
822
- shutdownRequested = true;
1269
+ setShutdownRequested(true);
823
1270
  if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
824
1271
  if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
825
1272
  server.close();
@@ -849,6 +1296,10 @@ async function serve(config) {
849
1296
  await new Promise(() => {});
850
1297
  }
851
1298
 
1299
+ //#endregion
1300
+ //#region src/daemon/daemon.ts
1301
+ var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
1302
+
852
1303
  //#endregion
853
1304
  export { serve as n, daemon_exports as t };
854
- //# sourceMappingURL=daemon-D9evGlgR.mjs.map
1305
+ //# sourceMappingURL=daemon-D3hYb5_C.mjs.map