@tekmidian/pai 0.9.13 → 0.9.14

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 (67) hide show
  1. package/dist/{auto-route-DL4ViDKC.mjs → auto-route-D54Kv7yI.mjs} +2 -2
  2. package/dist/{auto-route-DL4ViDKC.mjs.map → auto-route-D54Kv7yI.mjs.map} +1 -1
  3. package/dist/cli/index.mjs +808 -345
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/{clusters-CRlPBpq8.mjs → clusters-BwmdKB-V.mjs} +1 -1
  6. package/dist/{clusters-CRlPBpq8.mjs.map → clusters-BwmdKB-V.mjs.map} +1 -1
  7. package/dist/{config-BuhHWyOK.mjs → config-DqBY3aT0.mjs} +1 -1
  8. package/dist/{config-BuhHWyOK.mjs.map → config-DqBY3aT0.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +12 -8
  10. package/dist/daemon/index.mjs.map +1 -1
  11. package/dist/daemon-BryEC3as.mjs +1426 -0
  12. package/dist/daemon-BryEC3as.mjs.map +1 -0
  13. package/dist/daemon-mcp/index.mjs +2 -2
  14. package/dist/{detector-AzVtGLtL.mjs → detector-Q_lVXtj-.mjs} +1 -1
  15. package/dist/{detector-AzVtGLtL.mjs.map → detector-Q_lVXtj-.mjs.map} +1 -1
  16. package/dist/{factory-BDBIfTDC.mjs → factory-BufouUQ1.mjs} +3 -3
  17. package/dist/{factory-BDBIfTDC.mjs.map → factory-BufouUQ1.mjs.map} +1 -1
  18. package/dist/hooks/context-compression-hook.mjs +39 -0
  19. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  20. package/dist/hooks/stop-hook.mjs +36 -0
  21. package/dist/hooks/stop-hook.mjs.map +2 -2
  22. package/dist/index.mjs +2 -2
  23. package/dist/indexer-D7MvSQPY.mjs +1 -0
  24. package/dist/{indexer-backend-DXkDh-kc.mjs → indexer-backend-BrrCnasS.mjs} +1 -1
  25. package/dist/{indexer-backend-DXkDh-kc.mjs.map → indexer-backend-BrrCnasS.mjs.map} +1 -1
  26. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-C3pjwy2m.mjs} +1 -1
  27. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-C3pjwy2m.mjs.map} +1 -1
  28. package/dist/{kg-entity-D5v7RCDi.mjs → kg-entity-DVzTsy6G.mjs} +1 -1
  29. package/dist/{kg-entity-D5v7RCDi.mjs.map → kg-entity-DVzTsy6G.mjs.map} +1 -1
  30. package/dist/kg-extraction-r5KkTmWY.mjs +524 -0
  31. package/dist/kg-extraction-r5KkTmWY.mjs.map +1 -0
  32. package/dist/{latent-ideas-DvWBRHsy.mjs → latent-ideas-dWVuI_dg.mjs} +2 -2
  33. package/dist/{latent-ideas-DvWBRHsy.mjs.map → latent-ideas-dWVuI_dg.mjs.map} +1 -1
  34. package/dist/{migrate-jokLenje.mjs → migrate-fLD6rAdO.mjs} +2 -2
  35. package/dist/{migrate-jokLenje.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  36. package/dist/{neighborhood-u8ytjmWq.mjs → neighborhood-DixmpfPT.mjs} +1 -1
  37. package/dist/{neighborhood-u8ytjmWq.mjs.map → neighborhood-DixmpfPT.mjs.map} +1 -1
  38. package/dist/{note-context-CG2_e-0W.mjs → note-context-BY7rfGdl.mjs} +1 -1
  39. package/dist/{note-context-CG2_e-0W.mjs.map → note-context-BY7rfGdl.mjs.map} +1 -1
  40. package/dist/{postgres-BGERehmX.mjs → postgres-Cp25xZes.mjs} +1 -1
  41. package/dist/{postgres-BGERehmX.mjs.map → postgres-Cp25xZes.mjs.map} +1 -1
  42. package/dist/{query-feedback-CQSumXDy.mjs → query-feedback-C0hKUHVz.mjs} +1 -1
  43. package/dist/{query-feedback-CQSumXDy.mjs.map → query-feedback-C0hKUHVz.mjs.map} +1 -1
  44. package/dist/{sqlite-xUe94oUZ.mjs → sqlite-B3JHfJc2.mjs} +1 -1
  45. package/dist/{sqlite-xUe94oUZ.mjs.map → sqlite-B3JHfJc2.mjs.map} +1 -1
  46. package/dist/{state-BIlxNRUn.mjs → state-BHZAhLou.mjs} +1 -1
  47. package/dist/{state-BIlxNRUn.mjs.map → state-BHZAhLou.mjs.map} +1 -1
  48. package/dist/{themes-9jxFn3Rf.mjs → themes-DO8mUVwD.mjs} +1 -1
  49. package/dist/{themes-9jxFn3Rf.mjs.map → themes-DO8mUVwD.mjs.map} +1 -1
  50. package/dist/{tools-CF0lB5lW.mjs → tools-C1JNgya7.mjs} +14 -14
  51. package/dist/{tools-CF0lB5lW.mjs.map → tools-C1JNgya7.mjs.map} +1 -1
  52. package/dist/{trace-C2XrzssW.mjs → trace-TYCLMCkY.mjs} +1 -1
  53. package/dist/{trace-C2XrzssW.mjs.map → trace-TYCLMCkY.mjs.map} +1 -1
  54. package/dist/{utils-QSfKagcj.mjs → utils-BAxjW3j8.mjs} +2 -2
  55. package/dist/{utils-QSfKagcj.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  56. package/dist/{vault-indexer-iHRjcy6e.mjs → vault-indexer-DsPC6wQf.mjs} +1 -1
  57. package/dist/{vault-indexer-iHRjcy6e.mjs.map → vault-indexer-DsPC6wQf.mjs.map} +1 -1
  58. package/dist/{daemon-w2fE3UMz.mjs → work-queue-worker-94INtUiz.mjs} +54 -1398
  59. package/dist/work-queue-worker-94INtUiz.mjs.map +1 -0
  60. package/dist/{zettelkasten-BdaMzTGQ.mjs → zettelkasten-B-94vghH.mjs} +3 -3
  61. package/dist/{zettelkasten-BdaMzTGQ.mjs.map → zettelkasten-B-94vghH.mjs.map} +1 -1
  62. package/package.json +1 -1
  63. package/src/hooks/ts/pre-compact/context-compression-hook.ts +48 -0
  64. package/src/hooks/ts/stop/stop-hook.ts +40 -0
  65. package/dist/daemon-w2fE3UMz.mjs.map +0 -1
  66. package/dist/kg-extraction-Uvr9fTlz.mjs +0 -246
  67. package/dist/kg-extraction-Uvr9fTlz.mjs.map +0 -1
@@ -1,792 +1,16 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
- import { n as openRegistry } from "./db-BtuN768f.mjs";
3
2
  import { n as openFederation } from "./db-CYmBWcjh.mjs";
4
- import { d as sha256 } from "./helpers-OCVFgprQ.mjs";
5
- import { n as indexAll } from "./sync-CdHSL9Kc.mjs";
6
- import { t as configureEmbeddingModel } from "./embeddings-DGRAPAYb.mjs";
7
- import { t as extractAndStoreTriples$1 } from "./kg-extraction-Uvr9fTlz.mjs";
8
- import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-BuhHWyOK.mjs";
9
- import { t as createStorageBackend } from "./factory-BDBIfTDC.mjs";
10
- 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-BIlxNRUn.mjs";
11
- import { a as toolMemoryWakeup, c as toolSessionRoute, d as toolProjectInfo, f as toolProjectList, h as toolMemorySearch, i as toolMemoryTaxonomy, l as toolProjectDetect, m as toolMemoryGet, n as toolMemoryKgSearch, o as toolRegistrySearch, p as toolProjectTodo, r as toolMemoryFeedback, s as toolSessionList, u as toolProjectHealth } from "./tools-CF0lB5lW.mjs";
12
- import { t as detectTopicShift } from "./detector-AzVtGLtL.mjs";
3
+ import { i as performScan, t as extractAndStoreTriples$1 } from "./kg-extraction-r5KkTmWY.mjs";
4
+ import { O as storageBackend, t as daemonConfig, u as registryDb } from "./state-BHZAhLou.mjs";
5
+ import { t as detectTopicShift } from "./detector-Q_lVXtj-.mjs";
13
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
- import { homedir, setPriority } from "node:os";
7
+ import { homedir } from "node:os";
15
8
  import { basename, dirname, join } from "node:path";
16
9
  import { randomUUID } from "node:crypto";
17
- import { connect, createServer } from "node:net";
18
- import { spawn } from "node:child_process";
19
10
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, writeFileSync as writeFileSync$1 } from "fs";
20
11
  import { basename as basename$1, join as join$1, resolve as resolve$1 } from "path";
21
12
  import { homedir as homedir$1 } from "os";
22
13
 
23
- //#region src/notifications/config.ts
24
- /**
25
- * config.ts — Notification config persistence helpers
26
- *
27
- * Reads and writes the `notifications` section of ~/.config/pai/config.json.
28
- * Deep-merges with defaults so partial configs work fine.
29
- *
30
- * This module is intentionally separate from the daemon's config loader
31
- * so it can be used standalone (e.g. from CLI commands).
32
- */
33
- function deepMerge(target, source) {
34
- const result = { ...target };
35
- for (const key of Object.keys(source)) {
36
- const srcVal = source[key];
37
- if (srcVal === void 0 || srcVal === null) continue;
38
- const tgtVal = target[key];
39
- if (typeof srcVal === "object" && !Array.isArray(srcVal) && typeof tgtVal === "object" && tgtVal !== null && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
40
- else result[key] = srcVal;
41
- }
42
- return result;
43
- }
44
- /**
45
- * Load the notification config from the PAI config file.
46
- * Returns defaults merged with any stored values.
47
- */
48
- function loadNotificationConfig() {
49
- if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_NOTIFICATION_CONFIG };
50
- let raw;
51
- try {
52
- raw = readFileSync(CONFIG_FILE, "utf-8");
53
- } catch {
54
- return { ...DEFAULT_NOTIFICATION_CONFIG };
55
- }
56
- let parsed;
57
- try {
58
- parsed = JSON.parse(raw);
59
- } catch {
60
- return { ...DEFAULT_NOTIFICATION_CONFIG };
61
- }
62
- const stored = parsed["notifications"];
63
- if (!stored || typeof stored !== "object") return { ...DEFAULT_NOTIFICATION_CONFIG };
64
- return deepMerge(DEFAULT_NOTIFICATION_CONFIG, stored);
65
- }
66
- /**
67
- * Persist the notification config by merging it into the existing
68
- * ~/.config/pai/config.json. Creates the file if it does not exist.
69
- */
70
- function saveNotificationConfig(config) {
71
- if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
72
- let full = {};
73
- if (existsSync(CONFIG_FILE)) try {
74
- full = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
75
- } catch {}
76
- full["notifications"] = config;
77
- writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2) + "\n", "utf-8");
78
- }
79
- /**
80
- * Apply a partial update to the current notification config and persist it.
81
- * Returns the new merged config.
82
- */
83
- function patchNotificationConfig(patch) {
84
- const current = loadNotificationConfig();
85
- if (patch.mode !== void 0) current.mode = patch.mode;
86
- if (patch.channels) current.channels = deepMerge(current.channels, patch.channels);
87
- if (patch.routing) current.routing = deepMerge(current.routing, patch.routing);
88
- saveNotificationConfig(current);
89
- return current;
90
- }
91
-
92
- //#endregion
93
- //#region src/daemon/daemon/scheduler.ts
94
- /**
95
- * Index, embed, and vault index schedulers for the PAI daemon.
96
- * Exports run* functions (also called on-demand by the IPC handler)
97
- * and the start* functions invoked once at daemon startup.
98
- */
99
- /** Minimum interval between vault index runs (30 minutes). */
100
- const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
101
- /**
102
- * Run a full index pass. Guards against overlapping runs with indexInProgress.
103
- * Called both by the scheduler and by the index_now IPC method.
104
- */
105
- async function runIndex() {
106
- if (indexInProgress) {
107
- process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
108
- return;
109
- }
110
- if (embedInProgress) {
111
- process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
112
- return;
113
- }
114
- setIndexInProgress(true);
115
- const t0 = Date.now();
116
- try {
117
- process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
118
- if (storageBackend.backendType === "sqlite") {
119
- const { SQLiteBackend } = await import("./sqlite-xUe94oUZ.mjs");
120
- if (storageBackend instanceof SQLiteBackend) {
121
- const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
122
- const elapsed = Date.now() - t0;
123
- setLastIndexTime(Date.now());
124
- process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
125
- }
126
- } else {
127
- const { indexAllWithBackend } = await import("./indexer-backend-DXkDh-kc.mjs");
128
- const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
129
- const elapsed = Date.now() - t0;
130
- setLastIndexTime(Date.now());
131
- process.stderr.write(`[pai-daemon] Index complete (postgres): ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
132
- }
133
- } catch (e) {
134
- const msg = e instanceof Error ? e.message : String(e);
135
- process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
136
- } finally {
137
- setIndexInProgress(false);
138
- }
139
- }
140
- /**
141
- * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
142
- * Skips if no vaultPath is configured, or if project index/embed is in progress.
143
- */
144
- async function runVaultIndex() {
145
- if (!daemonConfig.vaultPath) return;
146
- if (vaultIndexInProgress) {
147
- process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
148
- return;
149
- }
150
- if (indexInProgress || embedInProgress) {
151
- process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
152
- return;
153
- }
154
- const { lastVaultIndexTime } = await import("./state-BIlxNRUn.mjs").then((n) => n.D);
155
- if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
156
- let vaultProjectId = daemonConfig.vaultProjectId;
157
- if (!vaultProjectId) {
158
- const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
159
- vaultProjectId = row?.id ?? 999;
160
- if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
161
- }
162
- setVaultIndexInProgress(true);
163
- const t0 = Date.now();
164
- process.stderr.write("[pai-daemon] Starting vault index run...\n");
165
- try {
166
- const { indexVault } = await import("./vault-indexer-iHRjcy6e.mjs");
167
- const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
168
- const elapsed = Date.now() - t0;
169
- setLastVaultIndexTime(Date.now());
170
- process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
171
- } catch (e) {
172
- const msg = e instanceof Error ? e.message : String(e);
173
- process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
174
- } finally {
175
- setVaultIndexInProgress(false);
176
- }
177
- }
178
- /**
179
- * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.
180
- */
181
- function startIndexScheduler() {
182
- const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
183
- process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
184
- setTimeout(() => {
185
- runIndex().then(() => runVaultIndex()).catch((e) => {
186
- process.stderr.write(`[pai-daemon] Startup index error: ${e}\n`);
187
- });
188
- }, 2e3);
189
- const timer = setInterval(() => {
190
- runIndex().then(() => runVaultIndex()).catch((e) => {
191
- process.stderr.write(`[pai-daemon] Scheduled index error: ${e}\n`);
192
- });
193
- }, intervalMs);
194
- if (timer.unref) timer.unref();
195
- setIndexSchedulerTimer(timer);
196
- }
197
- /**
198
- * Run an embedding pass for all unembedded chunks (Postgres backend only).
199
- */
200
- async function runEmbed() {
201
- if (embedInProgress) {
202
- process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
203
- return;
204
- }
205
- if (indexInProgress) {
206
- process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
207
- return;
208
- }
209
- if (storageBackend.backendType !== "postgres") return;
210
- setEmbedInProgress(true);
211
- const t0 = Date.now();
212
- try {
213
- process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
214
- const projectNames = /* @__PURE__ */ new Map();
215
- try {
216
- const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
217
- for (const r of rows) projectNames.set(r.id, r.slug);
218
- } catch {}
219
- const { embedChunksWithBackend } = await import("./indexer-backend-DXkDh-kc.mjs");
220
- const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames);
221
- let vaultEmbedCount = 0;
222
- if (daemonConfig.vaultPath) try {
223
- const { SQLiteBackend } = await import("./sqlite-xUe94oUZ.mjs");
224
- const { openFederation } = await import("./db-CYmBWcjh.mjs").then((n) => n.t);
225
- const federationDb = openFederation();
226
- const vaultSqliteBackend = new SQLiteBackend(federationDb);
227
- const vaultProjectNames = new Map(projectNames);
228
- if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
229
- vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames);
230
- try {
231
- federationDb.close();
232
- } catch {}
233
- if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
234
- } catch (ve) {
235
- const vmsg = ve instanceof Error ? ve.message : String(ve);
236
- process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
237
- }
238
- const elapsed = Date.now() - t0;
239
- setLastEmbedTime(Date.now());
240
- process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
241
- } catch (e) {
242
- const msg = e instanceof Error ? e.message : String(e);
243
- process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
244
- } finally {
245
- setEmbedInProgress(false);
246
- }
247
- }
248
- /**
249
- * Start the periodic embed scheduler. Initial run is 60 seconds after startup.
250
- */
251
- function startEmbedScheduler() {
252
- const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
253
- process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
254
- setTimeout(() => {
255
- runEmbed().catch((e) => {
256
- process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
257
- });
258
- }, 6e4);
259
- const timer = setInterval(() => {
260
- runEmbed().catch((e) => {
261
- process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
262
- });
263
- }, intervalMs);
264
- if (timer.unref) timer.unref();
265
- setEmbedSchedulerTimer(timer);
266
- }
267
-
268
- //#endregion
269
- //#region src/notifications/providers/ntfy.ts
270
- var NtfyProvider = class {
271
- channelId = "ntfy";
272
- async send(payload, config) {
273
- const cfg = config.channels.ntfy;
274
- if (!cfg.enabled || !cfg.url) return false;
275
- try {
276
- const headers = { "Content-Type": "text/plain; charset=utf-8" };
277
- if (payload.title) headers["Title"] = payload.title;
278
- if (cfg.priority && cfg.priority !== "default") headers["Priority"] = cfg.priority;
279
- return (await fetch(cfg.url, {
280
- method: "POST",
281
- headers,
282
- body: payload.message
283
- })).ok;
284
- } catch {
285
- return false;
286
- }
287
- }
288
- };
289
-
290
- //#endregion
291
- //#region src/notifications/providers/whatsapp.ts
292
- /**
293
- * whatsapp.ts — WhatsApp notification provider (via Whazaa MCP)
294
- *
295
- * Sends notifications via the Whazaa Unix Domain Socket IPC protocol.
296
- * Falls back gracefully if Whazaa is not running.
297
- *
298
- * Whazaa IPC socket: /tmp/whazaa.sock (standard Whazaa path)
299
- *
300
- * We use the same connect-per-call pattern as PaiClient to avoid
301
- * requiring any persistent connection state.
302
- */
303
- const WHAZAA_SOCKET = "/tmp/whazaa.sock";
304
- const WHAZAA_TIMEOUT_MS = 1e4;
305
- /**
306
- * Send a single IPC call to the Whazaa socket.
307
- * Returns true on success, false if Whazaa is not available or errors.
308
- */
309
- function callWhazaa(method, params) {
310
- return new Promise((resolve) => {
311
- let done = false;
312
- let buffer = "";
313
- let timer = null;
314
- function finish(ok) {
315
- if (done) return;
316
- done = true;
317
- if (timer) {
318
- clearTimeout(timer);
319
- timer = null;
320
- }
321
- try {
322
- socket?.destroy();
323
- } catch {}
324
- resolve(ok);
325
- }
326
- const socket = connect(WHAZAA_SOCKET, () => {
327
- const request = {
328
- jsonrpc: "2.0",
329
- id: randomUUID(),
330
- method,
331
- params
332
- };
333
- socket.write(JSON.stringify(request) + "\n");
334
- });
335
- socket.on("data", (chunk) => {
336
- buffer += chunk.toString();
337
- const nl = buffer.indexOf("\n");
338
- if (nl === -1) return;
339
- try {
340
- finish(!JSON.parse(buffer.slice(0, nl)).error);
341
- } catch {
342
- finish(false);
343
- }
344
- });
345
- socket.on("error", () => finish(false));
346
- socket.on("end", () => finish(false));
347
- timer = setTimeout(() => finish(false), WHAZAA_TIMEOUT_MS);
348
- });
349
- }
350
- var WhatsAppProvider = class {
351
- channelId = "whatsapp";
352
- async send(payload, config) {
353
- const cfg = config.channels.whatsapp;
354
- if (!cfg.enabled) return false;
355
- const isVoiceMode = config.mode === "voice" || config.channels.voice.enabled;
356
- const params = { message: payload.message };
357
- if (cfg.recipient) params.recipient = cfg.recipient;
358
- if (isVoiceMode && config.mode === "voice") params.voice = config.channels.voice.voiceName ?? "bm_george";
359
- return callWhazaa("whatsapp_send", params);
360
- }
361
- };
362
-
363
- //#endregion
364
- //#region src/notifications/providers/macos.ts
365
- /**
366
- * macos.ts — macOS notification provider
367
- *
368
- * Uses the `osascript` command to display a macOS system notification.
369
- * Non-blocking: spawns the process and returns success without waiting.
370
- */
371
- var MacOsProvider = class {
372
- channelId = "macos";
373
- async send(payload, config) {
374
- if (!config.channels.macos.enabled) return false;
375
- try {
376
- const safeTitle = (payload.title ?? "PAI").replace(/'/g, "\\'");
377
- const script = `display notification "${payload.message.replace(/'/g, "\\'")}" with title "${safeTitle}"`;
378
- return new Promise((resolve) => {
379
- const child = spawn("osascript", ["-e", script], {
380
- detached: true,
381
- stdio: "ignore"
382
- });
383
- child.unref();
384
- child.on("error", () => resolve(false));
385
- setTimeout(() => resolve(true), 200);
386
- });
387
- } catch {
388
- return false;
389
- }
390
- }
391
- };
392
-
393
- //#endregion
394
- //#region src/notifications/providers/cli.ts
395
- var CliProvider = class {
396
- channelId = "cli";
397
- async send(payload, _config) {
398
- const prefix = `[pai-notify:${payload.event}]`;
399
- const title = payload.title ? ` ${payload.title}:` : "";
400
- process.stderr.write(`${prefix}${title} ${payload.message}\n`);
401
- return true;
402
- }
403
- };
404
-
405
- //#endregion
406
- //#region src/notifications/router.ts
407
- const PROVIDERS = {
408
- ntfy: new NtfyProvider(),
409
- whatsapp: new WhatsAppProvider(),
410
- macos: new MacOsProvider(),
411
- voice: new WhatsAppProvider(),
412
- cli: new CliProvider()
413
- };
414
- /**
415
- * Given the current config, resolve which channels should receive a
416
- * notification for the given event type.
417
- *
418
- * Mode overrides:
419
- * "off" → no channels
420
- * "auto" → use routing table, filtered by enabled channels
421
- * "voice" → whatsapp (TTS enabled in provider)
422
- * "whatsapp" → whatsapp
423
- * "ntfy" → ntfy
424
- * "macos" → macos
425
- * "cli" → cli
426
- */
427
- function resolveChannels(config, event) {
428
- const { mode, channels, routing } = config;
429
- if (mode === "off") return [];
430
- const modeToChannel = {
431
- voice: "whatsapp",
432
- whatsapp: "whatsapp",
433
- ntfy: "ntfy",
434
- macos: "macos",
435
- cli: "cli"
436
- };
437
- if (mode !== "auto") {
438
- const ch = modeToChannel[mode];
439
- if (!ch) return [];
440
- const cfg = channels[ch];
441
- if (cfg && !cfg.enabled) return [ch];
442
- return [ch];
443
- }
444
- return (routing[event] ?? []).filter((ch) => {
445
- const cfg = channels[ch];
446
- if (ch === "voice") return false;
447
- return cfg?.enabled === true;
448
- });
449
- }
450
- /**
451
- * Route a notification to the appropriate channels.
452
- *
453
- * Sends to all resolved channels in parallel.
454
- * Individual channel failures are non-fatal and logged to stderr.
455
- *
456
- * @param payload The notification to send
457
- * @param config The current notification config (from daemon state)
458
- */
459
- async function routeNotification(payload, config) {
460
- const channels = resolveChannels(config, payload.event);
461
- if (channels.length === 0) return {
462
- channelsAttempted: [],
463
- channelsSucceeded: [],
464
- channelsFailed: [],
465
- mode: config.mode
466
- };
467
- const results = await Promise.allSettled(channels.map(async (ch) => {
468
- const ok = await PROVIDERS[ch].send(payload, config);
469
- if (!ok) process.stderr.write(`[pai-notify] Channel ${ch} failed for event ${payload.event}\n`);
470
- return {
471
- ch,
472
- ok
473
- };
474
- }));
475
- const succeeded = [];
476
- const failed = [];
477
- for (const r of results) if (r.status === "fulfilled") if (r.value.ok) succeeded.push(r.value.ch);
478
- else failed.push(r.value.ch);
479
- else failed.push(channels[results.indexOf(r)]);
480
- return {
481
- channelsAttempted: channels,
482
- channelsSucceeded: succeeded,
483
- channelsFailed: failed,
484
- mode: config.mode
485
- };
486
- }
487
-
488
- //#endregion
489
- //#region src/observations/store.ts
490
- /**
491
- * store.ts — PostgreSQL persistence for PAI observations.
492
- *
493
- * All functions accept a pg.Pool and are safe to call concurrently.
494
- * Schema is initialized lazily via ensureObservationTables().
495
- *
496
- * Content-hash deduplication: observations with the same hash
497
- * created within a 30-second window are silently dropped to prevent
498
- * duplicate entries from rapid repeated tool calls.
499
- */
500
- let _tablesEnsured = false;
501
- /**
502
- * Inlined schema DDL — avoids runtime file reads that break in bundled code
503
- * (the bundler puts this in a shared chunk whose __dirname differs from src/).
504
- */
505
- const SCHEMA_SQL = `
506
- CREATE TABLE IF NOT EXISTS pai_observations (
507
- id SERIAL PRIMARY KEY,
508
- session_id TEXT NOT NULL,
509
- project_id INTEGER,
510
- project_slug TEXT,
511
- type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
512
- title TEXT NOT NULL,
513
- narrative TEXT,
514
- tool_name TEXT,
515
- tool_input_summary TEXT,
516
- files_read JSONB DEFAULT '[]'::jsonb,
517
- files_modified JSONB DEFAULT '[]'::jsonb,
518
- concepts JSONB DEFAULT '[]'::jsonb,
519
- content_hash TEXT,
520
- created_at TIMESTAMPTZ DEFAULT NOW()
521
- );
522
-
523
- CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
524
- CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
525
- CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
526
- CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
527
- CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
528
-
529
- CREATE TABLE IF NOT EXISTS pai_session_summaries (
530
- id SERIAL PRIMARY KEY,
531
- session_id TEXT NOT NULL UNIQUE,
532
- project_id INTEGER,
533
- project_slug TEXT,
534
- request TEXT,
535
- investigated TEXT,
536
- learned TEXT,
537
- completed TEXT,
538
- next_steps TEXT,
539
- observation_count INTEGER DEFAULT 0,
540
- created_at TIMESTAMPTZ DEFAULT NOW()
541
- );
542
-
543
- CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
544
- CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
545
- `;
546
- /**
547
- * Run schema DDL idempotently against the given pool.
548
- * Uses a module-level flag so subsequent calls are no-ops within the same
549
- * process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
550
- */
551
- async function ensureObservationTables(pool) {
552
- if (_tablesEnsured) return;
553
- await pool.query(SCHEMA_SQL);
554
- _tablesEnsured = true;
555
- }
556
- /**
557
- * Compute a 16-character hex content hash for deduplication.
558
- * Hash = SHA256(session_id + tool_name + title).slice(0, 16)
559
- */
560
- function computeContentHash(sessionId, toolName, title) {
561
- return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
562
- }
563
- /**
564
- * Insert an observation, skipping duplicates within a 30-second window.
565
- * Returns the inserted row's id, or null if the insert was suppressed.
566
- */
567
- async function storeObservation(pool, obs) {
568
- await ensureObservationTables(pool);
569
- const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
570
- const dupCheck = await pool.query(`SELECT id FROM pai_observations
571
- WHERE content_hash = $1
572
- AND session_id = $2
573
- AND created_at >= NOW() - INTERVAL '30 seconds'
574
- LIMIT 1`, [hash, obs.session_id]);
575
- if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
576
- return (await pool.query(`INSERT INTO pai_observations
577
- (session_id, project_id, project_slug, type, title, narrative,
578
- tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
579
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
580
- RETURNING id`, [
581
- obs.session_id,
582
- obs.project_id ?? null,
583
- obs.project_slug ?? null,
584
- obs.type,
585
- obs.title,
586
- obs.narrative ?? null,
587
- obs.tool_name,
588
- obs.tool_input_summary ?? null,
589
- JSON.stringify(obs.files_read),
590
- JSON.stringify(obs.files_modified),
591
- JSON.stringify(obs.concepts),
592
- hash
593
- ])).rows[0]?.id ?? null;
594
- }
595
- /**
596
- * Filtered query for observations with optional projectId, sessionId, type,
597
- * limit, and offset. Returns results ordered by created_at DESC.
598
- */
599
- async function queryObservations(pool, opts = {}) {
600
- await ensureObservationTables(pool);
601
- const conditions = [];
602
- const params = [];
603
- let idx = 1;
604
- if (opts.projectId !== void 0) {
605
- conditions.push(`project_id = $${idx++}`);
606
- params.push(opts.projectId);
607
- }
608
- if (opts.sessionId !== void 0) {
609
- conditions.push(`session_id = $${idx++}`);
610
- params.push(opts.sessionId);
611
- }
612
- if (opts.type !== void 0) {
613
- conditions.push(`type = $${idx++}`);
614
- params.push(opts.type);
615
- }
616
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
617
- const limit = opts.limit ?? 50;
618
- const offset = opts.offset ?? 0;
619
- params.push(limit, offset);
620
- return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
621
- tool_name, tool_input_summary,
622
- files_read, files_modified, concepts,
623
- content_hash, created_at
624
- FROM pai_observations
625
- ${where}
626
- ORDER BY created_at DESC
627
- LIMIT $${idx++} OFFSET $${idx}`, params)).rows;
628
- }
629
- /**
630
- * Most recent observations for a project, ordered by created_at DESC.
631
- */
632
- async function queryRecentObservations(pool, projectId, limit) {
633
- await ensureObservationTables(pool);
634
- return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
635
- tool_name, tool_input_summary,
636
- files_read, files_modified, concepts,
637
- content_hash, created_at
638
- FROM pai_observations
639
- WHERE project_id = $1
640
- ORDER BY created_at DESC
641
- LIMIT $2`, [projectId, limit])).rows;
642
- }
643
- /**
644
- * Upsert a session summary. Uses ON CONFLICT on session_id so calling this
645
- * multiple times with updated content is safe.
646
- */
647
- async function storeSessionSummary(pool, summary) {
648
- await ensureObservationTables(pool);
649
- await pool.query(`INSERT INTO pai_session_summaries
650
- (session_id, project_id, project_slug, request, investigated,
651
- learned, completed, next_steps, observation_count)
652
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
653
- ON CONFLICT (session_id) DO UPDATE SET
654
- project_id = EXCLUDED.project_id,
655
- project_slug = EXCLUDED.project_slug,
656
- request = EXCLUDED.request,
657
- investigated = EXCLUDED.investigated,
658
- learned = EXCLUDED.learned,
659
- completed = EXCLUDED.completed,
660
- next_steps = EXCLUDED.next_steps,
661
- observation_count = EXCLUDED.observation_count`, [
662
- summary.session_id,
663
- summary.project_id ?? null,
664
- summary.project_slug ?? null,
665
- summary.request ?? null,
666
- summary.investigated ?? null,
667
- summary.learned ?? null,
668
- summary.completed ?? null,
669
- summary.next_steps ?? null,
670
- summary.observation_count ?? 0
671
- ]);
672
- }
673
-
674
- //#endregion
675
- //#region src/daemon/daemon/dispatcher.ts
676
- /**
677
- * Tool dispatcher — maps IPC method names to PAI tool functions.
678
- */
679
- /**
680
- * Dispatch an IPC tool call to the appropriate tool function.
681
- * Returns the tool result or throws on unknown/failed methods.
682
- */
683
- async function dispatchTool(method, params) {
684
- const p = params;
685
- switch (method) {
686
- case "memory_search": return toolMemorySearch(registryDb, storageBackend, p);
687
- case "memory_get": return toolMemoryGet(registryDb, p);
688
- case "project_info": return toolProjectInfo(registryDb, p);
689
- case "project_list": return toolProjectList(registryDb, p);
690
- case "session_list": return toolSessionList(registryDb, p);
691
- case "registry_search": return toolRegistrySearch(registryDb, p);
692
- case "project_detect": return toolProjectDetect(registryDb, p);
693
- case "project_health": return toolProjectHealth(registryDb, p);
694
- case "project_todo": return toolProjectTodo(registryDb, p);
695
- case "memory_wakeup": return toolMemoryWakeup(registryDb, p);
696
- case "memory_taxonomy": return toolMemoryTaxonomy(registryDb, storageBackend, p);
697
- case "topic_check": return detectTopicShift(registryDb, storageBackend, p);
698
- case "session_auto_route": return toolSessionRoute(registryDb, storageBackend, p);
699
- case "zettel_explore":
700
- case "zettel_health":
701
- case "zettel_surprise":
702
- case "zettel_suggest":
703
- case "zettel_converse":
704
- case "zettel_themes":
705
- case "zettel_god_notes":
706
- case "zettel_communities": {
707
- const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import("./tools-CF0lB5lW.mjs").then((n) => n.t);
708
- switch (method) {
709
- case "zettel_explore": return toolZettelExplore(storageBackend, p);
710
- case "zettel_health": return toolZettelHealth(storageBackend, p);
711
- case "zettel_surprise": return toolZettelSurprise(storageBackend, p);
712
- case "zettel_suggest": return toolZettelSuggest(storageBackend, p);
713
- case "zettel_converse": return toolZettelConverse(storageBackend, p);
714
- case "zettel_themes": return toolZettelThemes(storageBackend, p);
715
- case "zettel_god_notes": return toolZettelGodNotes(storageBackend, p);
716
- case "zettel_communities": return toolZettelCommunities(storageBackend, p);
717
- }
718
- break;
719
- }
720
- case "graph_clusters": {
721
- const { handleGraphClusters } = await import("./clusters-CRlPBpq8.mjs");
722
- return handleGraphClusters(storageBackend.getPool?.() ?? null, storageBackend, p);
723
- }
724
- case "graph_neighborhood": {
725
- const { handleGraphNeighborhood } = await import("./neighborhood-u8ytjmWq.mjs");
726
- return handleGraphNeighborhood(storageBackend.getPool?.() ?? null, storageBackend, p);
727
- }
728
- case "graph_note_context": {
729
- const { handleGraphNoteContext } = await import("./note-context-CG2_e-0W.mjs");
730
- return handleGraphNoteContext(storageBackend.getPool?.() ?? null, storageBackend, p);
731
- }
732
- case "graph_trace": {
733
- const { handleGraphTrace } = await import("./trace-C2XrzssW.mjs");
734
- return handleGraphTrace(storageBackend, p);
735
- }
736
- case "graph_latent_ideas": {
737
- const { handleGraphLatentIdeas } = await import("./latent-ideas-DvWBRHsy.mjs");
738
- return handleGraphLatentIdeas(storageBackend, p);
739
- }
740
- case "idea_materialize": {
741
- const { handleIdeaMaterialize } = await import("./latent-ideas-DvWBRHsy.mjs");
742
- if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
743
- return handleIdeaMaterialize(p, daemonConfig.vaultPath);
744
- }
745
- case "kg_add":
746
- case "kg_query":
747
- case "kg_invalidate":
748
- case "kg_contradictions": {
749
- const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-CF0lB5lW.mjs").then((n) => n.t);
750
- const pgPool = storageBackend.getPool?.() ?? null;
751
- if (!pgPool) throw new Error(`${method} requires a Postgres storage backend`);
752
- switch (method) {
753
- case "kg_add": return toolKgAdd(pgPool, p);
754
- case "kg_query": return toolKgQuery(pgPool, p);
755
- case "kg_invalidate": return toolKgInvalidate(pgPool, p);
756
- case "kg_contradictions": return toolKgContradictions(pgPool, p);
757
- }
758
- break;
759
- }
760
- case "memory_tunnels": {
761
- const { toolMemoryTunnels } = await import("./tools-CF0lB5lW.mjs").then((n) => n.t);
762
- return toolMemoryTunnels(registryDb, storageBackend, p);
763
- }
764
- case "memory_feedback": {
765
- const federationDb = openFederation();
766
- try {
767
- return await toolMemoryFeedback(federationDb, p);
768
- } finally {
769
- federationDb.close();
770
- }
771
- }
772
- case "memory_kg_search": {
773
- const federationDb = openFederation();
774
- const pgPool = storageBackend.getPool?.() ?? null;
775
- if (!pgPool) {
776
- federationDb.close();
777
- throw new Error("memory_kg_search requires a Postgres storage backend for KG triple expansion");
778
- }
779
- try {
780
- return await toolMemoryKgSearch(federationDb, pgPool, p);
781
- } finally {
782
- federationDb.close();
783
- }
784
- }
785
- default: throw new Error(`Unknown method: ${method}`);
786
- }
787
- }
788
-
789
- //#endregion
790
14
  //#region src/daemon/work-queue.ts
791
15
  /**
792
16
  * work-queue.ts — Persistent work queue for the PAI Daemon
@@ -963,6 +187,13 @@ function getStats() {
963
187
  return stats;
964
188
  }
965
189
  /**
190
+ * Returns true if any pending or processing item of the given type exists.
191
+ * Used for debouncing work items that are expensive to run concurrently.
192
+ */
193
+ function hasPendingOrProcessingOfType(type) {
194
+ return _queue.some((i) => i.type === type && (i.status === "pending" || i.status === "processing"));
195
+ }
196
+ /**
966
197
  * Remove completed and permanently-failed items older than their TTL.
967
198
  * Also force-cleans all completed items if the queue file exceeds 1 MB.
968
199
  */
@@ -2351,11 +1582,18 @@ async function handleTopicDetect(payload) {
2351
1582
  * Handles 'session-end' work items by reading the transcript, extracting
2352
1583
  * work summaries, updating the session note, and updating TODO.md.
2353
1584
  * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.
1585
+ * Handles 'registry-scan' items by running performScan() against the registry DB.
2354
1586
  *
2355
1587
  * Handles 'topic-detect' items by running BM25-based topic shift detection.
2356
1588
  * Other item types (note-update, todo-update) are stubs — they log and
2357
1589
  * complete immediately, ready for future expansion.
2358
1590
  */
1591
+ var work_queue_worker_exports = /* @__PURE__ */ __exportAll({
1592
+ enqueueRegistryScan: () => enqueueRegistryScan,
1593
+ notifyNewWork: () => notifyNewWork,
1594
+ startWorker: () => startWorker,
1595
+ stopWorker: () => stopWorker
1596
+ });
2359
1597
  const WORKER_INTERVAL_MS = 5e3;
2360
1598
  const HOUSEKEEPING_INTERVAL_MS = 600 * 1e3;
2361
1599
  let workerTimer = null;
@@ -2412,6 +1650,9 @@ async function processNextItem() {
2412
1650
  case "topic-detect":
2413
1651
  await handleTopicDetect(item.payload);
2414
1652
  break;
1653
+ case "registry-scan":
1654
+ await handleRegistryScan();
1655
+ break;
2415
1656
  case "note-update":
2416
1657
  case "todo-update":
2417
1658
  process.stderr.write(`[work-queue-worker] Item type '${item.type}' is not yet implemented — completing as no-op.\n`);
@@ -2487,6 +1728,37 @@ async function handleSessionEnd(item) {
2487
1728
  process.stderr.write(`[work-queue-worker] Could not move session files: ${moveError}\n`);
2488
1729
  }
2489
1730
  }
1731
+ /**
1732
+ * Run performScan() against the registry DB.
1733
+ *
1734
+ * Debounce: if another registry-scan job is already pending in the queue,
1735
+ * we still run this one (dequeue already picked it), but enqueueRegistryScan()
1736
+ * checks before enqueuing so duplicates rarely reach here.
1737
+ */
1738
+ async function handleRegistryScan() {
1739
+ if (!registryDb) throw new Error("registry-scan: registryDb not initialized yet");
1740
+ const t0 = Date.now();
1741
+ process.stderr.write("[work-queue-worker] Running registry scan...\n");
1742
+ const result = performScan(registryDb);
1743
+ const elapsed = Date.now() - t0;
1744
+ process.stderr.write(`[work-queue-worker] Registry scan complete: ${result.projectsScanned} projects (${result.projectsNew} new, ${result.projectsUpdated} updated), ${result.sessionsScanned} sessions (${result.sessionsNew} new) in ${elapsed}ms.\n`);
1745
+ if (result.skipped.length > 0) process.stderr.write(`[work-queue-worker] Registry scan: ${result.skipped.length} project(s) skipped (path not found).\n`);
1746
+ }
1747
+ /**
1748
+ * Enqueue a registry-scan work item — debounced.
1749
+ * If a registry-scan item is already pending or processing, skip enqueue.
1750
+ */
1751
+ function enqueueRegistryScan() {
1752
+ if (hasPendingOrProcessingOfType("registry-scan")) {
1753
+ process.stderr.write("[work-queue-worker] Registry scan already pending/processing — skipping duplicate enqueue.\n");
1754
+ return;
1755
+ }
1756
+ enqueue({
1757
+ type: "registry-scan",
1758
+ priority: 5,
1759
+ payload: {}
1760
+ });
1761
+ }
2490
1762
  function tryParseJson(line) {
2491
1763
  try {
2492
1764
  return JSON.parse(line);
@@ -2548,621 +1820,5 @@ function extractWorkFromTranscript(lines) {
2548
1820
  }
2549
1821
 
2550
1822
  //#endregion
2551
- //#region src/daemon/daemon/handler.ts
2552
- function sendResponse(socket, response) {
2553
- try {
2554
- socket.write(JSON.stringify(response) + "\n");
2555
- } catch {}
2556
- }
2557
- /**
2558
- * Handle a single IPC request.
2559
- */
2560
- async function handleRequest(request, socket) {
2561
- const { id, method, params } = request;
2562
- if (method === "status") {
2563
- const dbStats = await (async () => {
2564
- try {
2565
- const fedStats = await storageBackend.getStats();
2566
- const projects = registryDb.prepare("SELECT COUNT(*) AS n FROM projects").get().n;
2567
- return {
2568
- files: fedStats.files,
2569
- chunks: fedStats.chunks,
2570
- projects
2571
- };
2572
- } catch {
2573
- return null;
2574
- }
2575
- })();
2576
- sendResponse(socket, {
2577
- id,
2578
- ok: true,
2579
- result: {
2580
- uptime: Math.floor((Date.now() - startTime) / 1e3),
2581
- indexInProgress,
2582
- lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,
2583
- indexIntervalSecs: daemonConfig.indexIntervalSecs,
2584
- embedInProgress,
2585
- lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,
2586
- embedIntervalSecs: daemonConfig.embedIntervalSecs,
2587
- socketPath: daemonConfig.socketPath,
2588
- storageBackend: storageBackend.backendType,
2589
- db: dbStats,
2590
- vaultIndexInProgress,
2591
- lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,
2592
- vaultPath: daemonConfig.vaultPath ?? null,
2593
- workQueue: getStats()
2594
- }
2595
- });
2596
- socket.end();
2597
- return;
2598
- }
2599
- if (method === "index_now") {
2600
- runIndex().catch((e) => {
2601
- process.stderr.write(`[pai-daemon] index_now error: ${e}\n`);
2602
- });
2603
- sendResponse(socket, {
2604
- id,
2605
- ok: true,
2606
- result: { triggered: true }
2607
- });
2608
- socket.end();
2609
- return;
2610
- }
2611
- if (method === "vault_index_now") {
2612
- runVaultIndex().catch((e) => {
2613
- process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\n`);
2614
- });
2615
- sendResponse(socket, {
2616
- id,
2617
- ok: true,
2618
- result: { triggered: true }
2619
- });
2620
- socket.end();
2621
- return;
2622
- }
2623
- if (method === "notification_get_config") {
2624
- sendResponse(socket, {
2625
- id,
2626
- ok: true,
2627
- result: {
2628
- config: notificationConfig,
2629
- activeChannels: Object.entries(notificationConfig.channels).filter(([ch, cfg]) => ch !== "voice" && cfg.enabled).map(([ch]) => ch)
2630
- }
2631
- });
2632
- socket.end();
2633
- return;
2634
- }
2635
- if (method === "notification_set_config") {
2636
- try {
2637
- const p = params;
2638
- const updated = patchNotificationConfig({
2639
- mode: p.mode,
2640
- channels: p.channels,
2641
- routing: p.routing
2642
- });
2643
- setNotificationConfig(updated);
2644
- sendResponse(socket, {
2645
- id,
2646
- ok: true,
2647
- result: { config: updated }
2648
- });
2649
- } catch (e) {
2650
- sendResponse(socket, {
2651
- id,
2652
- ok: false,
2653
- error: e instanceof Error ? e.message : String(e)
2654
- });
2655
- }
2656
- socket.end();
2657
- return;
2658
- }
2659
- if (method === "notification_send") {
2660
- const p = params;
2661
- if (!p.message) {
2662
- sendResponse(socket, {
2663
- id,
2664
- ok: false,
2665
- error: "notification_send: message is required"
2666
- });
2667
- socket.end();
2668
- return;
2669
- }
2670
- routeNotification({
2671
- event: p.event ?? "info",
2672
- message: p.message,
2673
- title: p.title
2674
- }, notificationConfig).then((result) => {
2675
- sendResponse(socket, {
2676
- id,
2677
- ok: true,
2678
- result
2679
- });
2680
- socket.end();
2681
- }).catch((e) => {
2682
- sendResponse(socket, {
2683
- id,
2684
- ok: false,
2685
- error: e instanceof Error ? e.message : String(e)
2686
- });
2687
- socket.end();
2688
- });
2689
- return;
2690
- }
2691
- if (method === "observation_store") {
2692
- const pool = storageBackend.getPool?.();
2693
- if (!pool) {
2694
- sendResponse(socket, {
2695
- id,
2696
- ok: false,
2697
- error: "Observations require Postgres backend"
2698
- });
2699
- socket.end();
2700
- return;
2701
- }
2702
- try {
2703
- const p = params;
2704
- let project_id = null;
2705
- let project_slug = null;
2706
- if (p.cwd) {
2707
- 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);
2708
- if (row) {
2709
- project_id = row.id;
2710
- project_slug = row.slug;
2711
- }
2712
- }
2713
- await ensureObservationTables(pool);
2714
- sendResponse(socket, {
2715
- id,
2716
- ok: true,
2717
- result: {
2718
- ok: true,
2719
- id: await storeObservation(pool, {
2720
- session_id: p.session_id,
2721
- project_id,
2722
- project_slug,
2723
- type: p.type,
2724
- title: p.title,
2725
- narrative: p.narrative ?? null,
2726
- tool_name: p.tool_name,
2727
- tool_input_summary: p.tool_input_summary ?? null,
2728
- files_read: p.files_read ?? [],
2729
- files_modified: p.files_modified ?? [],
2730
- concepts: p.concepts ?? []
2731
- })
2732
- }
2733
- });
2734
- } catch (e) {
2735
- sendResponse(socket, {
2736
- id,
2737
- ok: false,
2738
- error: e instanceof Error ? e.message : String(e)
2739
- });
2740
- }
2741
- socket.end();
2742
- return;
2743
- }
2744
- if (method === "observation_query") {
2745
- const pool = storageBackend.getPool?.();
2746
- if (!pool) {
2747
- sendResponse(socket, {
2748
- id,
2749
- ok: false,
2750
- error: "Observations require Postgres backend"
2751
- });
2752
- socket.end();
2753
- return;
2754
- }
2755
- try {
2756
- const p = params;
2757
- sendResponse(socket, {
2758
- id,
2759
- ok: true,
2760
- result: await queryObservations(pool, {
2761
- projectId: p.project_id,
2762
- sessionId: p.session_id,
2763
- type: p.type,
2764
- limit: p.limit,
2765
- offset: p.offset
2766
- })
2767
- });
2768
- } catch (e) {
2769
- sendResponse(socket, {
2770
- id,
2771
- ok: false,
2772
- error: e instanceof Error ? e.message : String(e)
2773
- });
2774
- }
2775
- socket.end();
2776
- return;
2777
- }
2778
- if (method === "observation_recent") {
2779
- const pool = storageBackend.getPool?.();
2780
- if (!pool) {
2781
- sendResponse(socket, {
2782
- id,
2783
- ok: false,
2784
- error: "Observations require Postgres backend"
2785
- });
2786
- socket.end();
2787
- return;
2788
- }
2789
- try {
2790
- const p = params;
2791
- const limit = p.limit ?? 20;
2792
- let resolvedProjectId = p.project_id;
2793
- let resolvedProjectSlug;
2794
- if (resolvedProjectId === void 0 && p.cwd) {
2795
- 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);
2796
- if (row) {
2797
- resolvedProjectId = row.id;
2798
- resolvedProjectSlug = row.slug;
2799
- }
2800
- }
2801
- let rows;
2802
- if (resolvedProjectId !== void 0) rows = await queryRecentObservations(pool, resolvedProjectId, limit);
2803
- else rows = await queryObservations(pool, { limit });
2804
- sendResponse(socket, {
2805
- id,
2806
- ok: true,
2807
- result: {
2808
- rows,
2809
- project_slug: resolvedProjectSlug
2810
- }
2811
- });
2812
- } catch (e) {
2813
- sendResponse(socket, {
2814
- id,
2815
- ok: false,
2816
- error: e instanceof Error ? e.message : String(e)
2817
- });
2818
- }
2819
- socket.end();
2820
- return;
2821
- }
2822
- if (method === "observation_list") {
2823
- const pool = storageBackend.getPool?.();
2824
- if (!pool) {
2825
- sendResponse(socket, {
2826
- id,
2827
- ok: false,
2828
- error: "Observations require Postgres backend"
2829
- });
2830
- socket.end();
2831
- return;
2832
- }
2833
- try {
2834
- const p = params;
2835
- let projectId;
2836
- if (p.project_slug) projectId = registryDb.prepare("SELECT id FROM projects WHERE slug = ?").get(p.project_slug)?.id;
2837
- sendResponse(socket, {
2838
- id,
2839
- ok: true,
2840
- result: await queryObservations(pool, {
2841
- projectId,
2842
- sessionId: p.session_id,
2843
- type: p.type,
2844
- limit: p.limit ?? 20,
2845
- offset: p.offset ?? 0
2846
- })
2847
- });
2848
- } catch (e) {
2849
- sendResponse(socket, {
2850
- id,
2851
- ok: false,
2852
- error: e instanceof Error ? e.message : String(e)
2853
- });
2854
- }
2855
- socket.end();
2856
- return;
2857
- }
2858
- if (method === "observation_stats") {
2859
- const pool = storageBackend.getPool?.();
2860
- if (!pool) {
2861
- sendResponse(socket, {
2862
- id,
2863
- ok: false,
2864
- error: "Observations require Postgres backend"
2865
- });
2866
- socket.end();
2867
- return;
2868
- }
2869
- try {
2870
- await ensureObservationTables(pool);
2871
- const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([
2872
- pool.query("SELECT COUNT(*) as count FROM pai_observations"),
2873
- pool.query("SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC"),
2874
- pool.query("SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15"),
2875
- pool.query("SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1")
2876
- ]);
2877
- sendResponse(socket, {
2878
- id,
2879
- ok: true,
2880
- result: {
2881
- total: parseInt(totalRes.rows[0]?.count ?? "0", 10),
2882
- by_type: byTypeRes.rows.map((r) => ({
2883
- type: r.type,
2884
- count: parseInt(r.count, 10)
2885
- })),
2886
- by_project: byProjectRes.rows.map((r) => ({
2887
- project_slug: r.project_slug,
2888
- count: parseInt(r.count, 10)
2889
- })),
2890
- most_recent: recentRes.rows[0]?.created_at ?? null
2891
- }
2892
- });
2893
- } catch (e) {
2894
- sendResponse(socket, {
2895
- id,
2896
- ok: false,
2897
- error: e instanceof Error ? e.message : String(e)
2898
- });
2899
- }
2900
- socket.end();
2901
- return;
2902
- }
2903
- if (method === "session_summary_store") {
2904
- const pool = storageBackend.getPool?.();
2905
- if (!pool) {
2906
- sendResponse(socket, {
2907
- id,
2908
- ok: false,
2909
- error: "Session summaries require Postgres backend"
2910
- });
2911
- socket.end();
2912
- return;
2913
- }
2914
- try {
2915
- const p = params;
2916
- let resolvedProjectId = p.project_id ?? null;
2917
- let resolvedProjectSlug = p.project_slug ?? null;
2918
- if (resolvedProjectId === null && p.cwd) {
2919
- 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);
2920
- if (row) {
2921
- resolvedProjectId = row.id;
2922
- resolvedProjectSlug = row.slug;
2923
- }
2924
- }
2925
- await storeSessionSummary(pool, {
2926
- session_id: p.session_id,
2927
- project_id: resolvedProjectId,
2928
- project_slug: resolvedProjectSlug,
2929
- request: p.request ?? null,
2930
- investigated: p.investigated ?? null,
2931
- learned: p.learned ?? null,
2932
- completed: p.completed ?? null,
2933
- next_steps: p.next_steps ?? null,
2934
- observation_count: p.observation_count ?? 0
2935
- });
2936
- sendResponse(socket, {
2937
- id,
2938
- ok: true,
2939
- result: { ok: true }
2940
- });
2941
- } catch (e) {
2942
- sendResponse(socket, {
2943
- id,
2944
- ok: false,
2945
- error: e instanceof Error ? e.message : String(e)
2946
- });
2947
- }
2948
- socket.end();
2949
- return;
2950
- }
2951
- if (method === "work_queue_enqueue") {
2952
- try {
2953
- const p = params;
2954
- if (!p.type) {
2955
- sendResponse(socket, {
2956
- id,
2957
- ok: false,
2958
- error: "work_queue_enqueue: type is required"
2959
- });
2960
- socket.end();
2961
- return;
2962
- }
2963
- const item = enqueue({
2964
- type: p.type,
2965
- priority: p.priority,
2966
- payload: p.payload ?? {},
2967
- maxAttempts: p.maxAttempts
2968
- });
2969
- /* @__PURE__ */ notifyNewWork();
2970
- sendResponse(socket, {
2971
- id,
2972
- ok: true,
2973
- result: {
2974
- id: item.id,
2975
- status: item.status
2976
- }
2977
- });
2978
- } catch (e) {
2979
- sendResponse(socket, {
2980
- id,
2981
- ok: false,
2982
- error: e instanceof Error ? e.message : String(e)
2983
- });
2984
- }
2985
- socket.end();
2986
- return;
2987
- }
2988
- if (method === "work_queue_stats") {
2989
- sendResponse(socket, {
2990
- id,
2991
- ok: true,
2992
- result: getStats()
2993
- });
2994
- socket.end();
2995
- return;
2996
- }
2997
- try {
2998
- sendResponse(socket, {
2999
- id,
3000
- ok: true,
3001
- result: await dispatchTool(method, params)
3002
- });
3003
- } catch (e) {
3004
- sendResponse(socket, {
3005
- id,
3006
- ok: false,
3007
- error: e instanceof Error ? e.message : String(e)
3008
- });
3009
- }
3010
- socket.end();
3011
- }
3012
-
3013
- //#endregion
3014
- //#region src/daemon/daemon/server.ts
3015
- /**
3016
- * IPC server and daemon entry point.
3017
- * Owns: isSocketLive, startIpcServer, serve (exported).
3018
- */
3019
- /**
3020
- * Check whether an existing socket file is actually being served by a live process.
3021
- */
3022
- function isSocketLive(path) {
3023
- return new Promise((resolve) => {
3024
- const client = connect(path);
3025
- const timer = setTimeout(() => {
3026
- client.destroy();
3027
- resolve(false);
3028
- }, 500);
3029
- client.on("connect", () => {
3030
- clearTimeout(timer);
3031
- client.end();
3032
- resolve(true);
3033
- });
3034
- client.on("error", () => {
3035
- clearTimeout(timer);
3036
- resolve(false);
3037
- });
3038
- });
3039
- }
3040
- /**
3041
- * Start the Unix Domain Socket IPC server.
3042
- */
3043
- async function startIpcServer(socketPath) {
3044
- if (existsSync(socketPath)) {
3045
- if (await isSocketLive(socketPath)) throw new Error("Another daemon is already running — socket is live. Aborting startup.");
3046
- try {
3047
- unlinkSync(socketPath);
3048
- process.stderr.write("[pai-daemon] Removed stale socket file.\n");
3049
- } catch {}
3050
- }
3051
- const server = createServer((socket) => {
3052
- let buffer = "";
3053
- socket.on("data", (chunk) => {
3054
- buffer += chunk.toString();
3055
- let nl;
3056
- while ((nl = buffer.indexOf("\n")) !== -1) {
3057
- const line = buffer.slice(0, nl);
3058
- buffer = buffer.slice(nl + 1);
3059
- if (line.trim() === "") continue;
3060
- let request;
3061
- try {
3062
- request = JSON.parse(line);
3063
- } catch {
3064
- sendResponse(socket, {
3065
- id: "?",
3066
- ok: false,
3067
- error: "Invalid JSON"
3068
- });
3069
- socket.destroy();
3070
- return;
3071
- }
3072
- handleRequest(request, socket).catch((e) => {
3073
- const msg = e instanceof Error ? e.message : String(e);
3074
- sendResponse(socket, {
3075
- id: request.id,
3076
- ok: false,
3077
- error: msg
3078
- });
3079
- socket.destroy();
3080
- });
3081
- }
3082
- });
3083
- socket.on("error", () => {});
3084
- });
3085
- server.on("error", (e) => {
3086
- process.stderr.write(`[pai-daemon] IPC server error: ${e}\n`);
3087
- });
3088
- server.listen(socketPath, () => {
3089
- process.stderr.write(`[pai-daemon] IPC server listening on ${socketPath}\n`);
3090
- });
3091
- return server;
3092
- }
3093
- async function serve(config) {
3094
- setDaemonConfig(config);
3095
- setStartTime(Date.now());
3096
- setNotificationConfig(loadNotificationConfig());
3097
- process.stderr.write("[pai-daemon] Starting daemon...\n");
3098
- process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
3099
- process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
3100
- const { notificationConfig } = await import("./state-BIlxNRUn.mjs").then((n) => n.D);
3101
- process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
3102
- try {
3103
- setPriority(process.pid, 10);
3104
- } catch {}
3105
- configureEmbeddingModel(config.embeddingModel);
3106
- try {
3107
- setRegistryDb(openRegistry());
3108
- process.stderr.write("[pai-daemon] Registry database opened.\n");
3109
- } catch (e) {
3110
- const msg = e instanceof Error ? e.message : String(e);
3111
- process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\n`);
3112
- process.exit(1);
3113
- }
3114
- try {
3115
- const backend = await createStorageBackend(config);
3116
- setStorageBackend(backend);
3117
- process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
3118
- } catch (e) {
3119
- const msg = e instanceof Error ? e.message : String(e);
3120
- process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
3121
- process.exit(1);
3122
- }
3123
- startIndexScheduler();
3124
- if (storageBackend.backendType === "postgres") startEmbedScheduler();
3125
- else process.stderr.write("[pai-daemon] Embed scheduler: disabled (SQLite backend)\n");
3126
- loadQueue();
3127
- startWorker();
3128
- const server = await startIpcServer(config.socketPath);
3129
- const shutdown = async (signal) => {
3130
- process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
3131
- setShutdownRequested(true);
3132
- if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
3133
- if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
3134
- stopWorker();
3135
- server.close();
3136
- const SHUTDOWN_TIMEOUT_MS = 1e4;
3137
- const POLL_INTERVAL_MS = 100;
3138
- const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;
3139
- if (indexInProgress || embedInProgress) {
3140
- process.stderr.write(`[pai-daemon] Waiting for in-progress operations to finish (index=${indexInProgress}, embed=${embedInProgress})...\n`);
3141
- while ((indexInProgress || embedInProgress) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
3142
- if (indexInProgress || embedInProgress) process.stderr.write("[pai-daemon] Shutdown timeout reached — forcing exit.\n");
3143
- else process.stderr.write("[pai-daemon] In-progress operations finished.\n");
3144
- }
3145
- try {
3146
- await storageBackend.close();
3147
- } catch {}
3148
- try {
3149
- unlinkSync(config.socketPath);
3150
- } catch {}
3151
- process.exit(0);
3152
- };
3153
- process.on("SIGINT", () => {
3154
- shutdown("SIGINT").catch(() => process.exit(0));
3155
- });
3156
- process.on("SIGTERM", () => {
3157
- shutdown("SIGTERM").catch(() => process.exit(0));
3158
- });
3159
- await new Promise(() => {});
3160
- }
3161
-
3162
- //#endregion
3163
- //#region src/daemon/daemon.ts
3164
- var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
3165
-
3166
- //#endregion
3167
- export { serve as n, daemon_exports as t };
3168
- //# sourceMappingURL=daemon-w2fE3UMz.mjs.map
1823
+ export { enqueue as a, work_queue_worker_exports as i, startWorker as n, getStats as o, stopWorker as r, loadQueue as s, notifyNewWork as t };
1824
+ //# sourceMappingURL=work-queue-worker-94INtUiz.mjs.map