@tekmidian/pai 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-route-BWGvvpcP.mjs +86 -0
- package/dist/auto-route-BWGvvpcP.mjs.map +1 -0
- package/dist/cli/index.mjs +3 -3
- package/dist/cli/program.mjs +5 -4
- package/dist/cli/program.mjs.map +1 -1
- package/dist/clusters-BdvGIoD-.mjs +201 -0
- package/dist/clusters-BdvGIoD-.mjs.map +1 -0
- package/dist/daemon/index.mjs +6 -5
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BaBTOB1P.mjs +1356 -0
- package/dist/daemon-BaBTOB1P.mjs.map +1 -0
- package/dist/detector-DExEQ5cW.mjs +74 -0
- package/dist/detector-DExEQ5cW.mjs.map +1 -0
- package/dist/factory-CAy0N1xR.mjs +142 -0
- package/dist/factory-CAy0N1xR.mjs.map +1 -0
- package/dist/indexer-backend-vOJqSE0U.mjs +299 -0
- package/dist/indexer-backend-vOJqSE0U.mjs.map +1 -0
- package/dist/latent-ideas-DuM_kgkW.mjs +191 -0
- package/dist/latent-ideas-DuM_kgkW.mjs.map +1 -0
- package/dist/main-resolver-Bzloyzi0.mjs +1352 -0
- package/dist/main-resolver-Bzloyzi0.mjs.map +1 -0
- package/dist/neighborhood-DSkvFMAv.mjs +135 -0
- package/dist/neighborhood-DSkvFMAv.mjs.map +1 -0
- package/dist/note-context-DrcY4cWm.mjs +126 -0
- package/dist/note-context-DrcY4cWm.mjs.map +1 -0
- package/dist/pick-C_52sxvh.mjs +13336 -0
- package/dist/pick-C_52sxvh.mjs.map +1 -0
- package/dist/pick-E-q8lf6K.mjs +13336 -0
- package/dist/pick-E-q8lf6K.mjs.map +1 -0
- package/dist/pick-FrfZ_iY7.mjs +13299 -0
- package/dist/pick-FrfZ_iY7.mjs.map +1 -0
- package/dist/pick-sZo7IQi5.mjs +13350 -0
- package/dist/pick-sZo7IQi5.mjs.map +1 -0
- package/dist/postgres-DTyxU4B1.mjs +891 -0
- package/dist/postgres-DTyxU4B1.mjs.map +1 -0
- package/dist/query-feedback-BX5nSyRm.mjs +76 -0
- package/dist/query-feedback-BX5nSyRm.mjs.map +1 -0
- package/dist/router-B2xR3gVP.mjs +228 -0
- package/dist/router-B2xR3gVP.mjs.map +1 -0
- package/dist/sqlite--BBAyXLH.mjs +271 -0
- package/dist/sqlite--BBAyXLH.mjs.map +1 -0
- package/dist/themes-DICajLf-.mjs +148 -0
- package/dist/themes-DICajLf-.mjs.map +1 -0
- package/dist/tools-DMAQxlOk.mjs +1939 -0
- package/dist/tools-DMAQxlOk.mjs.map +1 -0
- package/dist/trace-DfyGmMG_.mjs +137 -0
- package/dist/trace-DfyGmMG_.mjs.map +1 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs +536 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs.map +1 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs +1856 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs.map +1 -0
- package/dist/zettelkasten-C8BikWss.mjs +1063 -0
- package/dist/zettelkasten-C8BikWss.mjs.map +1 -0
- package/docs/commands/pause.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
import { n as openRegistry } from "./db-BtuN768f.mjs";
|
|
3
|
+
import { n as openFederation } from "./db-CYmBWcjh.mjs";
|
|
4
|
+
import { d as sha256 } from "./helpers-crDEr6S2.mjs";
|
|
5
|
+
import { n as indexAll } from "./sync--BoxBBok.mjs";
|
|
6
|
+
import { t as configureEmbeddingModel } from "./embeddings-Bn86ssxR.mjs";
|
|
7
|
+
import { l as DEFAULT_NOTIFICATION_CONFIG, n as CONFIG_FILE, t as CONFIG_DIR } from "./config-CcdkNSWa.mjs";
|
|
8
|
+
import { r as getBackendOutage, t as createStorageBackend } from "./factory-CAy0N1xR.mjs";
|
|
9
|
+
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-DTvy-jRB.mjs";
|
|
10
|
+
import { t as routeNotification } from "./router-B2xR3gVP.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-DMAQxlOk.mjs";
|
|
12
|
+
import { t as detectTopicShift } from "./detector-DExEQ5cW.mjs";
|
|
13
|
+
import { a as enqueue, n as startWorker, o as getStats, r as stopWorker, s as loadQueue, t as notifyNewWork } from "./work-queue-worker-Bt_UcbMy.mjs";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { setPriority } from "node:os";
|
|
16
|
+
import { connect, createServer } from "node:net";
|
|
17
|
+
|
|
18
|
+
//#region src/notifications/config.ts
|
|
19
|
+
/**
|
|
20
|
+
* config.ts — Notification config persistence helpers
|
|
21
|
+
*
|
|
22
|
+
* Reads and writes the `notifications` section of ~/.config/pai/config.json.
|
|
23
|
+
* Deep-merges with defaults so partial configs work fine.
|
|
24
|
+
*
|
|
25
|
+
* This module is intentionally separate from the daemon's config loader
|
|
26
|
+
* so it can be used standalone (e.g. from CLI commands).
|
|
27
|
+
*/
|
|
28
|
+
function deepMerge(target, source) {
|
|
29
|
+
const result = { ...target };
|
|
30
|
+
for (const key of Object.keys(source)) {
|
|
31
|
+
const srcVal = source[key];
|
|
32
|
+
if (srcVal === void 0 || srcVal === null) continue;
|
|
33
|
+
const tgtVal = target[key];
|
|
34
|
+
if (typeof srcVal === "object" && !Array.isArray(srcVal) && typeof tgtVal === "object" && tgtVal !== null && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
|
|
35
|
+
else result[key] = srcVal;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load the notification config from the PAI config file.
|
|
41
|
+
* Returns defaults merged with any stored values.
|
|
42
|
+
*/
|
|
43
|
+
function loadNotificationConfig() {
|
|
44
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
48
|
+
} catch {
|
|
49
|
+
return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
56
|
+
}
|
|
57
|
+
const stored = parsed["notifications"];
|
|
58
|
+
if (!stored || typeof stored !== "object") return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
59
|
+
return deepMerge(DEFAULT_NOTIFICATION_CONFIG, stored);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Persist the notification config by merging it into the existing
|
|
63
|
+
* ~/.config/pai/config.json. Creates the file if it does not exist.
|
|
64
|
+
*/
|
|
65
|
+
function saveNotificationConfig(config) {
|
|
66
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
67
|
+
let full = {};
|
|
68
|
+
if (existsSync(CONFIG_FILE)) try {
|
|
69
|
+
full = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
70
|
+
} catch {}
|
|
71
|
+
full["notifications"] = config;
|
|
72
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2) + "\n", "utf-8");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Apply a partial update to the current notification config and persist it.
|
|
76
|
+
* Returns the new merged config.
|
|
77
|
+
*/
|
|
78
|
+
function patchNotificationConfig(patch) {
|
|
79
|
+
const current = loadNotificationConfig();
|
|
80
|
+
if (patch.mode !== void 0) current.mode = patch.mode;
|
|
81
|
+
if (patch.channels) current.channels = deepMerge(current.channels, patch.channels);
|
|
82
|
+
if (patch.routing) current.routing = deepMerge(current.routing, patch.routing);
|
|
83
|
+
saveNotificationConfig(current);
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/daemon/daemon/scheduler.ts
|
|
89
|
+
/**
|
|
90
|
+
* Index, embed, vault index, and registry scan schedulers for the PAI daemon.
|
|
91
|
+
* Exports run* functions (also called on-demand by the IPC handler)
|
|
92
|
+
* and the start* functions invoked once at daemon startup.
|
|
93
|
+
*/
|
|
94
|
+
/** Minimum interval between vault index runs (30 minutes). */
|
|
95
|
+
const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
|
|
96
|
+
/**
|
|
97
|
+
* Run a full index pass. Guards against overlapping runs with indexInProgress.
|
|
98
|
+
* Called both by the scheduler and by the index_now IPC method.
|
|
99
|
+
*/
|
|
100
|
+
async function runIndex() {
|
|
101
|
+
if (indexInProgress) {
|
|
102
|
+
process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (embedInProgress) {
|
|
106
|
+
process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setIndexInProgress(true);
|
|
110
|
+
const t0 = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
|
|
113
|
+
if (storageBackend.backendType === "sqlite") {
|
|
114
|
+
const { SQLiteBackend } = await import("./sqlite--BBAyXLH.mjs");
|
|
115
|
+
if (storageBackend instanceof SQLiteBackend) {
|
|
116
|
+
const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
|
|
117
|
+
const elapsed = Date.now() - t0;
|
|
118
|
+
setLastIndexTime(Date.now());
|
|
119
|
+
process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
const { indexAllWithBackend } = await import("./indexer-backend-vOJqSE0U.mjs");
|
|
123
|
+
const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
|
|
124
|
+
const elapsed = Date.now() - t0;
|
|
125
|
+
setLastIndexTime(Date.now());
|
|
126
|
+
process.stderr.write(`[pai-daemon] Index complete (postgres): ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
130
|
+
process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
|
|
131
|
+
} finally {
|
|
132
|
+
setIndexInProgress(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
|
|
137
|
+
* Skips if no vaultPath is configured, or if project index/embed is in progress.
|
|
138
|
+
*/
|
|
139
|
+
async function runVaultIndex() {
|
|
140
|
+
if (!daemonConfig.vaultPath) return;
|
|
141
|
+
if (vaultIndexInProgress) {
|
|
142
|
+
process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (indexInProgress || embedInProgress) {
|
|
146
|
+
process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { lastVaultIndexTime } = await import("./state-DTvy-jRB.mjs").then((n) => n.D);
|
|
150
|
+
if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
|
|
151
|
+
let vaultProjectId = daemonConfig.vaultProjectId;
|
|
152
|
+
if (!vaultProjectId) {
|
|
153
|
+
const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
|
|
154
|
+
vaultProjectId = row?.id ?? 999;
|
|
155
|
+
if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
|
|
156
|
+
}
|
|
157
|
+
setVaultIndexInProgress(true);
|
|
158
|
+
const t0 = Date.now();
|
|
159
|
+
process.stderr.write("[pai-daemon] Starting vault index run...\n");
|
|
160
|
+
try {
|
|
161
|
+
const { indexVault } = await import("./vault-indexer-Dt8qXP-w.mjs");
|
|
162
|
+
const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
|
|
163
|
+
const elapsed = Date.now() - t0;
|
|
164
|
+
setLastVaultIndexTime(Date.now());
|
|
165
|
+
process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
168
|
+
process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
|
|
169
|
+
} finally {
|
|
170
|
+
setVaultIndexInProgress(false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.
|
|
175
|
+
*/
|
|
176
|
+
function startIndexScheduler() {
|
|
177
|
+
const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
|
|
178
|
+
process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
|
|
179
|
+
const cycle = (label) => runIndex().then(() => runVaultIndex()).then(() => runEmbed()).catch((e) => {
|
|
180
|
+
process.stderr.write(`[pai-daemon] ${label} index error: ${e}\n`);
|
|
181
|
+
});
|
|
182
|
+
setTimeout(() => void cycle("Startup"), 2e3);
|
|
183
|
+
const timer = setInterval(() => void cycle("Scheduled"), intervalMs);
|
|
184
|
+
if (timer.unref) timer.unref();
|
|
185
|
+
setIndexSchedulerTimer(timer);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Run an embedding pass for all unembedded chunks (Postgres backend only).
|
|
189
|
+
*/
|
|
190
|
+
async function runEmbed() {
|
|
191
|
+
if (embedInProgress) {
|
|
192
|
+
process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (indexInProgress) {
|
|
196
|
+
process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (storageBackend.backendType !== "postgres") return;
|
|
200
|
+
setEmbedInProgress(true);
|
|
201
|
+
const t0 = Date.now();
|
|
202
|
+
try {
|
|
203
|
+
process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
|
|
204
|
+
const projectNames = /* @__PURE__ */ new Map();
|
|
205
|
+
try {
|
|
206
|
+
const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
|
|
207
|
+
for (const r of rows) projectNames.set(r.id, r.slug);
|
|
208
|
+
} catch {}
|
|
209
|
+
const { embedChunksWithBackend } = await import("./indexer-backend-vOJqSE0U.mjs");
|
|
210
|
+
const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames, { maxMillis: 24e4 });
|
|
211
|
+
let vaultEmbedCount = 0;
|
|
212
|
+
if (daemonConfig.vaultPath) try {
|
|
213
|
+
const { SQLiteBackend } = await import("./sqlite--BBAyXLH.mjs");
|
|
214
|
+
const { openFederation } = await import("./db-CYmBWcjh.mjs").then((n) => n.t);
|
|
215
|
+
const federationDb = openFederation();
|
|
216
|
+
const vaultSqliteBackend = new SQLiteBackend(federationDb);
|
|
217
|
+
const vaultProjectNames = new Map(projectNames);
|
|
218
|
+
if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
|
|
219
|
+
vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames, { maxMillis: 45e3 });
|
|
220
|
+
try {
|
|
221
|
+
federationDb.close();
|
|
222
|
+
} catch {}
|
|
223
|
+
if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
|
|
224
|
+
} catch (ve) {
|
|
225
|
+
const vmsg = ve instanceof Error ? ve.message : String(ve);
|
|
226
|
+
process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
|
|
227
|
+
}
|
|
228
|
+
const elapsed = Date.now() - t0;
|
|
229
|
+
setLastEmbedTime(Date.now());
|
|
230
|
+
process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
233
|
+
process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
|
|
234
|
+
} finally {
|
|
235
|
+
setEmbedInProgress(false);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Start the periodic embed scheduler. Initial run is 60 seconds after startup.
|
|
240
|
+
*/
|
|
241
|
+
function startEmbedScheduler() {
|
|
242
|
+
const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
|
|
243
|
+
process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
runEmbed().catch((e) => {
|
|
246
|
+
process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
|
|
247
|
+
});
|
|
248
|
+
}, 6e4);
|
|
249
|
+
const timer = setInterval(() => {
|
|
250
|
+
runEmbed().catch((e) => {
|
|
251
|
+
process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
|
|
252
|
+
});
|
|
253
|
+
}, intervalMs);
|
|
254
|
+
if (timer.unref) timer.unref();
|
|
255
|
+
setEmbedSchedulerTimer(timer);
|
|
256
|
+
}
|
|
257
|
+
const REGISTRY_SCAN_INTERVAL_MS = 1800 * 1e3;
|
|
258
|
+
const REGISTRY_SCAN_STARTUP_DELAY_MS = 1e4;
|
|
259
|
+
/**
|
|
260
|
+
* Start the periodic registry scan scheduler.
|
|
261
|
+
* First scan runs 10 seconds after startup (enough time for IPC to stabilise).
|
|
262
|
+
* Subsequent scans run every 30 minutes.
|
|
263
|
+
* Uses the work-queue so the scan runs on the worker thread without blocking IPC.
|
|
264
|
+
*/
|
|
265
|
+
function startRegistryScanScheduler() {
|
|
266
|
+
process.stderr.write("[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\n");
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
import("./work-queue-worker-Bt_UcbMy.mjs").then((n) => n.i).then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
269
|
+
process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\n`);
|
|
270
|
+
});
|
|
271
|
+
}, REGISTRY_SCAN_STARTUP_DELAY_MS);
|
|
272
|
+
const timer = setInterval(() => {
|
|
273
|
+
import("./work-queue-worker-Bt_UcbMy.mjs").then((n) => n.i).then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
274
|
+
process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\n`);
|
|
275
|
+
});
|
|
276
|
+
}, REGISTRY_SCAN_INTERVAL_MS);
|
|
277
|
+
if (timer.unref) timer.unref();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/observations/store.ts
|
|
282
|
+
/**
|
|
283
|
+
* store.ts — PostgreSQL persistence for PAI observations.
|
|
284
|
+
*
|
|
285
|
+
* All functions accept a pg.Pool and are safe to call concurrently.
|
|
286
|
+
* Schema is initialized lazily via ensureObservationTables().
|
|
287
|
+
*
|
|
288
|
+
* Content-hash deduplication: observations with the same hash
|
|
289
|
+
* created within a 30-second window are silently dropped to prevent
|
|
290
|
+
* duplicate entries from rapid repeated tool calls.
|
|
291
|
+
*/
|
|
292
|
+
let _tablesEnsured = false;
|
|
293
|
+
/**
|
|
294
|
+
* Inlined schema DDL — avoids runtime file reads that break in bundled code
|
|
295
|
+
* (the bundler puts this in a shared chunk whose __dirname differs from src/).
|
|
296
|
+
*/
|
|
297
|
+
const SCHEMA_SQL = `
|
|
298
|
+
CREATE TABLE IF NOT EXISTS pai_observations (
|
|
299
|
+
id SERIAL PRIMARY KEY,
|
|
300
|
+
session_id TEXT NOT NULL,
|
|
301
|
+
project_id INTEGER,
|
|
302
|
+
project_slug TEXT,
|
|
303
|
+
type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
|
304
|
+
title TEXT NOT NULL,
|
|
305
|
+
narrative TEXT,
|
|
306
|
+
tool_name TEXT,
|
|
307
|
+
tool_input_summary TEXT,
|
|
308
|
+
files_read JSONB DEFAULT '[]'::jsonb,
|
|
309
|
+
files_modified JSONB DEFAULT '[]'::jsonb,
|
|
310
|
+
concepts JSONB DEFAULT '[]'::jsonb,
|
|
311
|
+
content_hash TEXT,
|
|
312
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
|
|
318
|
+
CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
|
|
319
|
+
CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
|
|
320
|
+
|
|
321
|
+
CREATE TABLE IF NOT EXISTS pai_session_summaries (
|
|
322
|
+
id SERIAL PRIMARY KEY,
|
|
323
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
324
|
+
project_id INTEGER,
|
|
325
|
+
project_slug TEXT,
|
|
326
|
+
request TEXT,
|
|
327
|
+
investigated TEXT,
|
|
328
|
+
learned TEXT,
|
|
329
|
+
completed TEXT,
|
|
330
|
+
next_steps TEXT,
|
|
331
|
+
observation_count INTEGER DEFAULT 0,
|
|
332
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
|
|
337
|
+
`;
|
|
338
|
+
/**
|
|
339
|
+
* Run schema DDL idempotently against the given pool.
|
|
340
|
+
* Uses a module-level flag so subsequent calls are no-ops within the same
|
|
341
|
+
* process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
|
|
342
|
+
*/
|
|
343
|
+
async function ensureObservationTables(pool) {
|
|
344
|
+
if (_tablesEnsured) return;
|
|
345
|
+
await pool.query(SCHEMA_SQL);
|
|
346
|
+
_tablesEnsured = true;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Compute a 16-character hex content hash for deduplication.
|
|
350
|
+
* Hash = SHA256(session_id + tool_name + title).slice(0, 16)
|
|
351
|
+
*/
|
|
352
|
+
function computeContentHash(sessionId, toolName, title) {
|
|
353
|
+
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Insert an observation, skipping duplicates within a 30-second window.
|
|
357
|
+
* Returns the inserted row's id, or null if the insert was suppressed.
|
|
358
|
+
*/
|
|
359
|
+
async function storeObservation(pool, obs) {
|
|
360
|
+
await ensureObservationTables(pool);
|
|
361
|
+
const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
|
|
362
|
+
const dupCheck = await pool.query(`SELECT id FROM pai_observations
|
|
363
|
+
WHERE content_hash = $1
|
|
364
|
+
AND session_id = $2
|
|
365
|
+
AND created_at >= NOW() - INTERVAL '30 seconds'
|
|
366
|
+
LIMIT 1`, [hash, obs.session_id]);
|
|
367
|
+
if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
|
|
368
|
+
return (await pool.query(`INSERT INTO pai_observations
|
|
369
|
+
(session_id, project_id, project_slug, type, title, narrative,
|
|
370
|
+
tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
|
|
371
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
|
|
372
|
+
RETURNING id`, [
|
|
373
|
+
obs.session_id,
|
|
374
|
+
obs.project_id ?? null,
|
|
375
|
+
obs.project_slug ?? null,
|
|
376
|
+
obs.type,
|
|
377
|
+
obs.title,
|
|
378
|
+
obs.narrative ?? null,
|
|
379
|
+
obs.tool_name,
|
|
380
|
+
obs.tool_input_summary ?? null,
|
|
381
|
+
JSON.stringify(obs.files_read),
|
|
382
|
+
JSON.stringify(obs.files_modified),
|
|
383
|
+
JSON.stringify(obs.concepts),
|
|
384
|
+
hash
|
|
385
|
+
])).rows[0]?.id ?? null;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Filtered query for observations with optional projectId, sessionId, type,
|
|
389
|
+
* limit, and offset. Returns results ordered by created_at DESC.
|
|
390
|
+
*/
|
|
391
|
+
async function queryObservations(pool, opts = {}) {
|
|
392
|
+
await ensureObservationTables(pool);
|
|
393
|
+
const conditions = [];
|
|
394
|
+
const params = [];
|
|
395
|
+
let idx = 1;
|
|
396
|
+
if (opts.projectId !== void 0) {
|
|
397
|
+
conditions.push(`project_id = $${idx++}`);
|
|
398
|
+
params.push(opts.projectId);
|
|
399
|
+
}
|
|
400
|
+
if (opts.sessionId !== void 0) {
|
|
401
|
+
conditions.push(`session_id = $${idx++}`);
|
|
402
|
+
params.push(opts.sessionId);
|
|
403
|
+
}
|
|
404
|
+
if (opts.type !== void 0) {
|
|
405
|
+
conditions.push(`type = $${idx++}`);
|
|
406
|
+
params.push(opts.type);
|
|
407
|
+
}
|
|
408
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
409
|
+
const limit = opts.limit ?? 50;
|
|
410
|
+
const offset = opts.offset ?? 0;
|
|
411
|
+
params.push(limit, offset);
|
|
412
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
|
|
413
|
+
tool_name, tool_input_summary,
|
|
414
|
+
files_read, files_modified, concepts,
|
|
415
|
+
content_hash, created_at
|
|
416
|
+
FROM pai_observations
|
|
417
|
+
${where}
|
|
418
|
+
ORDER BY created_at DESC
|
|
419
|
+
LIMIT $${idx++} OFFSET $${idx}`, params)).rows;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Most recent observations for a project, ordered by created_at DESC.
|
|
423
|
+
*/
|
|
424
|
+
async function queryRecentObservations(pool, projectId, limit) {
|
|
425
|
+
await ensureObservationTables(pool);
|
|
426
|
+
return (await pool.query(`SELECT id, session_id, project_id, project_slug, type, title, narrative,
|
|
427
|
+
tool_name, tool_input_summary,
|
|
428
|
+
files_read, files_modified, concepts,
|
|
429
|
+
content_hash, created_at
|
|
430
|
+
FROM pai_observations
|
|
431
|
+
WHERE project_id = $1
|
|
432
|
+
ORDER BY created_at DESC
|
|
433
|
+
LIMIT $2`, [projectId, limit])).rows;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Upsert a session summary. Uses ON CONFLICT on session_id so calling this
|
|
437
|
+
* multiple times with updated content is safe.
|
|
438
|
+
*/
|
|
439
|
+
async function storeSessionSummary(pool, summary) {
|
|
440
|
+
await ensureObservationTables(pool);
|
|
441
|
+
await pool.query(`INSERT INTO pai_session_summaries
|
|
442
|
+
(session_id, project_id, project_slug, request, investigated,
|
|
443
|
+
learned, completed, next_steps, observation_count)
|
|
444
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
445
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
446
|
+
project_id = EXCLUDED.project_id,
|
|
447
|
+
project_slug = EXCLUDED.project_slug,
|
|
448
|
+
request = EXCLUDED.request,
|
|
449
|
+
investigated = EXCLUDED.investigated,
|
|
450
|
+
learned = EXCLUDED.learned,
|
|
451
|
+
completed = EXCLUDED.completed,
|
|
452
|
+
next_steps = EXCLUDED.next_steps,
|
|
453
|
+
observation_count = EXCLUDED.observation_count`, [
|
|
454
|
+
summary.session_id,
|
|
455
|
+
summary.project_id ?? null,
|
|
456
|
+
summary.project_slug ?? null,
|
|
457
|
+
summary.request ?? null,
|
|
458
|
+
summary.investigated ?? null,
|
|
459
|
+
summary.learned ?? null,
|
|
460
|
+
summary.completed ?? null,
|
|
461
|
+
summary.next_steps ?? null,
|
|
462
|
+
summary.observation_count ?? 0
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
const SKILL_TELEMETRY_SCHEMA_SQL = `
|
|
466
|
+
CREATE TABLE IF NOT EXISTS pai_skill_telemetry (
|
|
467
|
+
id SERIAL PRIMARY KEY,
|
|
468
|
+
scope TEXT NOT NULL DEFAULT 'default',
|
|
469
|
+
skill_name TEXT NOT NULL,
|
|
470
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
471
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('trial','active','archived')),
|
|
472
|
+
trigger_count INTEGER NOT NULL DEFAULT 0,
|
|
473
|
+
accept_count INTEGER NOT NULL DEFAULT 0,
|
|
474
|
+
first_triggered TIMESTAMPTZ DEFAULT NOW(),
|
|
475
|
+
last_triggered TIMESTAMPTZ DEFAULT NOW(),
|
|
476
|
+
context_projects JSONB DEFAULT '[]'::jsonb,
|
|
477
|
+
hash TEXT,
|
|
478
|
+
audit_status TEXT,
|
|
479
|
+
last_audited TIMESTAMPTZ,
|
|
480
|
+
UNIQUE(scope, skill_name, source)
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_scope ON pai_skill_telemetry(scope);
|
|
484
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_status ON pai_skill_telemetry(scope, status);
|
|
485
|
+
CREATE INDEX IF NOT EXISTS idx_skilltel_last ON pai_skill_telemetry(last_triggered DESC);
|
|
486
|
+
`;
|
|
487
|
+
let _skillTelemetryEnsured = false;
|
|
488
|
+
async function ensureSkillTelemetryTable(pool) {
|
|
489
|
+
if (_skillTelemetryEnsured) return;
|
|
490
|
+
await pool.query(SKILL_TELEMETRY_SCHEMA_SQL);
|
|
491
|
+
_skillTelemetryEnsured = true;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Record a single skill invocation. Upserts on (scope, skill_name, source):
|
|
495
|
+
* increments trigger_count, refreshes last_triggered, and dedup-merges the
|
|
496
|
+
* project slug into context_projects.
|
|
497
|
+
*/
|
|
498
|
+
async function recordSkillInvocation(pool, input) {
|
|
499
|
+
await ensureSkillTelemetryTable(pool);
|
|
500
|
+
const scope = input.scope ?? "default";
|
|
501
|
+
const source = input.source ?? "local";
|
|
502
|
+
const proj = input.project_slug ?? null;
|
|
503
|
+
const initialProjects = JSON.stringify(proj ? [proj] : []);
|
|
504
|
+
await pool.query(`INSERT INTO pai_skill_telemetry
|
|
505
|
+
(scope, skill_name, source, trigger_count, context_projects)
|
|
506
|
+
VALUES ($1, $2, $3, 1, $4::jsonb)
|
|
507
|
+
ON CONFLICT (scope, skill_name, source) DO UPDATE SET
|
|
508
|
+
trigger_count = pai_skill_telemetry.trigger_count + 1,
|
|
509
|
+
last_triggered = NOW(),
|
|
510
|
+
context_projects = CASE
|
|
511
|
+
WHEN $5::text IS NULL THEN pai_skill_telemetry.context_projects
|
|
512
|
+
WHEN pai_skill_telemetry.context_projects @> to_jsonb($5::text)
|
|
513
|
+
THEN pai_skill_telemetry.context_projects
|
|
514
|
+
ELSE pai_skill_telemetry.context_projects || to_jsonb($5::text)
|
|
515
|
+
END`, [
|
|
516
|
+
scope,
|
|
517
|
+
input.skill_name,
|
|
518
|
+
source,
|
|
519
|
+
initialProjects,
|
|
520
|
+
proj
|
|
521
|
+
]);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* List skill telemetry rows, most-triggered first.
|
|
525
|
+
*/
|
|
526
|
+
async function querySkillTelemetry(pool, opts = {}) {
|
|
527
|
+
await ensureSkillTelemetryTable(pool);
|
|
528
|
+
const conditions = [];
|
|
529
|
+
const params = [];
|
|
530
|
+
let idx = 1;
|
|
531
|
+
conditions.push(`scope = $${idx++}`);
|
|
532
|
+
params.push(opts.scope ?? "default");
|
|
533
|
+
if (opts.status !== void 0) {
|
|
534
|
+
conditions.push(`status = $${idx++}`);
|
|
535
|
+
params.push(opts.status);
|
|
536
|
+
}
|
|
537
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
538
|
+
const limit = opts.limit ?? 100;
|
|
539
|
+
params.push(limit);
|
|
540
|
+
return (await pool.query(`SELECT id, scope, skill_name, source, status,
|
|
541
|
+
trigger_count, accept_count,
|
|
542
|
+
first_triggered, last_triggered,
|
|
543
|
+
context_projects, hash, audit_status, last_audited
|
|
544
|
+
FROM pai_skill_telemetry
|
|
545
|
+
${where}
|
|
546
|
+
ORDER BY trigger_count DESC, last_triggered DESC
|
|
547
|
+
LIMIT $${idx}`, params)).rows;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/daemon/daemon/dispatcher.ts
|
|
552
|
+
/**
|
|
553
|
+
* Tool dispatcher — maps IPC method names to PAI tool functions.
|
|
554
|
+
*/
|
|
555
|
+
/**
|
|
556
|
+
* Dispatch an IPC tool call to the appropriate tool function.
|
|
557
|
+
* Returns the tool result or throws on unknown/failed methods.
|
|
558
|
+
*/
|
|
559
|
+
async function dispatchTool(method, params) {
|
|
560
|
+
const p = params;
|
|
561
|
+
switch (method) {
|
|
562
|
+
case "memory_search": return toolMemorySearch(registryDb, storageBackend, p);
|
|
563
|
+
case "memory_get": return toolMemoryGet(registryDb, p);
|
|
564
|
+
case "project_info": return toolProjectInfo(registryDb, p);
|
|
565
|
+
case "project_list": return toolProjectList(registryDb, p);
|
|
566
|
+
case "session_list": return toolSessionList(registryDb, p);
|
|
567
|
+
case "registry_search": return toolRegistrySearch(registryDb, p);
|
|
568
|
+
case "project_detect": return toolProjectDetect(registryDb, p);
|
|
569
|
+
case "project_health": return toolProjectHealth(registryDb, p);
|
|
570
|
+
case "project_todo": return toolProjectTodo(registryDb, p);
|
|
571
|
+
case "memory_wakeup": return toolMemoryWakeup(registryDb, p);
|
|
572
|
+
case "memory_taxonomy": return toolMemoryTaxonomy(registryDb, storageBackend, p);
|
|
573
|
+
case "topic_check": return detectTopicShift(registryDb, storageBackend, p);
|
|
574
|
+
case "session_auto_route": return toolSessionRoute(registryDb, storageBackend, p);
|
|
575
|
+
case "zettel_explore":
|
|
576
|
+
case "zettel_health":
|
|
577
|
+
case "zettel_surprise":
|
|
578
|
+
case "zettel_suggest":
|
|
579
|
+
case "zettel_converse":
|
|
580
|
+
case "zettel_themes":
|
|
581
|
+
case "zettel_god_notes":
|
|
582
|
+
case "zettel_communities": {
|
|
583
|
+
const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import("./tools-DMAQxlOk.mjs").then((n) => n.t);
|
|
584
|
+
switch (method) {
|
|
585
|
+
case "zettel_explore": return toolZettelExplore(storageBackend, p);
|
|
586
|
+
case "zettel_health": return toolZettelHealth(storageBackend, p);
|
|
587
|
+
case "zettel_surprise": return toolZettelSurprise(storageBackend, p);
|
|
588
|
+
case "zettel_suggest": return toolZettelSuggest(storageBackend, p);
|
|
589
|
+
case "zettel_converse": return toolZettelConverse(storageBackend, p);
|
|
590
|
+
case "zettel_themes": return toolZettelThemes(storageBackend, p);
|
|
591
|
+
case "zettel_god_notes": return toolZettelGodNotes(storageBackend, p);
|
|
592
|
+
case "zettel_communities": return toolZettelCommunities(storageBackend, p);
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
case "graph_clusters": {
|
|
597
|
+
const { handleGraphClusters } = await import("./clusters-BdvGIoD-.mjs");
|
|
598
|
+
return handleGraphClusters(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
599
|
+
}
|
|
600
|
+
case "graph_neighborhood": {
|
|
601
|
+
const { handleGraphNeighborhood } = await import("./neighborhood-DSkvFMAv.mjs");
|
|
602
|
+
return handleGraphNeighborhood(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
603
|
+
}
|
|
604
|
+
case "graph_note_context": {
|
|
605
|
+
const { handleGraphNoteContext } = await import("./note-context-DrcY4cWm.mjs");
|
|
606
|
+
return handleGraphNoteContext(storageBackend.getPool?.() ?? null, storageBackend, p);
|
|
607
|
+
}
|
|
608
|
+
case "graph_trace": {
|
|
609
|
+
const { handleGraphTrace } = await import("./trace-DfyGmMG_.mjs");
|
|
610
|
+
return handleGraphTrace(storageBackend, p);
|
|
611
|
+
}
|
|
612
|
+
case "graph_latent_ideas": {
|
|
613
|
+
const { handleGraphLatentIdeas } = await import("./latent-ideas-DuM_kgkW.mjs");
|
|
614
|
+
return handleGraphLatentIdeas(storageBackend, p);
|
|
615
|
+
}
|
|
616
|
+
case "idea_materialize": {
|
|
617
|
+
const { handleIdeaMaterialize } = await import("./latent-ideas-DuM_kgkW.mjs");
|
|
618
|
+
if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
|
|
619
|
+
return handleIdeaMaterialize(p, daemonConfig.vaultPath);
|
|
620
|
+
}
|
|
621
|
+
case "kg_add":
|
|
622
|
+
case "kg_query":
|
|
623
|
+
case "kg_invalidate":
|
|
624
|
+
case "kg_contradictions": {
|
|
625
|
+
const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-DMAQxlOk.mjs").then((n) => n.t);
|
|
626
|
+
const pgPool = storageBackend.getPool?.() ?? null;
|
|
627
|
+
if (!pgPool) throw new Error(`${method} requires a Postgres storage backend`);
|
|
628
|
+
switch (method) {
|
|
629
|
+
case "kg_add": return toolKgAdd(pgPool, p);
|
|
630
|
+
case "kg_query": return toolKgQuery(pgPool, p);
|
|
631
|
+
case "kg_invalidate": return toolKgInvalidate(pgPool, p);
|
|
632
|
+
case "kg_contradictions": return toolKgContradictions(pgPool, p);
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
case "memory_tunnels": {
|
|
637
|
+
const { toolMemoryTunnels } = await import("./tools-DMAQxlOk.mjs").then((n) => n.t);
|
|
638
|
+
return toolMemoryTunnels(registryDb, storageBackend, p);
|
|
639
|
+
}
|
|
640
|
+
case "memory_feedback": {
|
|
641
|
+
const federationDb = openFederation();
|
|
642
|
+
try {
|
|
643
|
+
return await toolMemoryFeedback(federationDb, p);
|
|
644
|
+
} finally {
|
|
645
|
+
federationDb.close();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
case "memory_kg_search": {
|
|
649
|
+
const federationDb = openFederation();
|
|
650
|
+
const pgPool = storageBackend.getPool?.() ?? null;
|
|
651
|
+
if (!pgPool) {
|
|
652
|
+
federationDb.close();
|
|
653
|
+
throw new Error("memory_kg_search requires a Postgres storage backend for KG triple expansion");
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
return await toolMemoryKgSearch(federationDb, pgPool, p);
|
|
657
|
+
} finally {
|
|
658
|
+
federationDb.close();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
default: throw new Error(`Unknown method: ${method}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/daemon/daemon/handler.ts
|
|
667
|
+
function sendResponse(socket, response) {
|
|
668
|
+
try {
|
|
669
|
+
socket.write(JSON.stringify(response) + "\n");
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Handle a single IPC request.
|
|
674
|
+
*/
|
|
675
|
+
async function handleRequest(request, socket) {
|
|
676
|
+
const { id, method, params } = request;
|
|
677
|
+
if (method === "status") {
|
|
678
|
+
const dbStats = await (async () => {
|
|
679
|
+
try {
|
|
680
|
+
if (!storageBackend) return null;
|
|
681
|
+
const fedStats = await storageBackend.getStats();
|
|
682
|
+
const projects = registryDb.prepare("SELECT COUNT(*) AS n FROM projects").get().n;
|
|
683
|
+
return {
|
|
684
|
+
files: fedStats.files,
|
|
685
|
+
chunks: fedStats.chunks,
|
|
686
|
+
projects
|
|
687
|
+
};
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
692
|
+
sendResponse(socket, {
|
|
693
|
+
id,
|
|
694
|
+
ok: true,
|
|
695
|
+
result: {
|
|
696
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
697
|
+
indexInProgress,
|
|
698
|
+
lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,
|
|
699
|
+
indexIntervalSecs: daemonConfig.indexIntervalSecs,
|
|
700
|
+
embedInProgress,
|
|
701
|
+
lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,
|
|
702
|
+
embedIntervalSecs: daemonConfig.embedIntervalSecs,
|
|
703
|
+
socketPath: daemonConfig.socketPath,
|
|
704
|
+
storageBackend: storageBackend?.backendType ?? "initializing",
|
|
705
|
+
db: dbStats,
|
|
706
|
+
vaultIndexInProgress,
|
|
707
|
+
lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,
|
|
708
|
+
vaultPath: daemonConfig.vaultPath ?? null,
|
|
709
|
+
workQueue: getStats(),
|
|
710
|
+
backendOutage: getBackendOutage()
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
socket.end();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (method === "index_now") {
|
|
717
|
+
runIndex().catch((e) => {
|
|
718
|
+
process.stderr.write(`[pai-daemon] index_now error: ${e}\n`);
|
|
719
|
+
});
|
|
720
|
+
sendResponse(socket, {
|
|
721
|
+
id,
|
|
722
|
+
ok: true,
|
|
723
|
+
result: { triggered: true }
|
|
724
|
+
});
|
|
725
|
+
socket.end();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (method === "vault_index_now") {
|
|
729
|
+
runVaultIndex().catch((e) => {
|
|
730
|
+
process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\n`);
|
|
731
|
+
});
|
|
732
|
+
sendResponse(socket, {
|
|
733
|
+
id,
|
|
734
|
+
ok: true,
|
|
735
|
+
result: { triggered: true }
|
|
736
|
+
});
|
|
737
|
+
socket.end();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (method === "notification_get_config") {
|
|
741
|
+
sendResponse(socket, {
|
|
742
|
+
id,
|
|
743
|
+
ok: true,
|
|
744
|
+
result: {
|
|
745
|
+
config: notificationConfig,
|
|
746
|
+
activeChannels: Object.entries(notificationConfig.channels).filter(([ch, cfg]) => ch !== "voice" && cfg.enabled).map(([ch]) => ch)
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
socket.end();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (method === "notification_set_config") {
|
|
753
|
+
try {
|
|
754
|
+
const p = params;
|
|
755
|
+
const updated = patchNotificationConfig({
|
|
756
|
+
mode: p.mode,
|
|
757
|
+
channels: p.channels,
|
|
758
|
+
routing: p.routing
|
|
759
|
+
});
|
|
760
|
+
setNotificationConfig(updated);
|
|
761
|
+
sendResponse(socket, {
|
|
762
|
+
id,
|
|
763
|
+
ok: true,
|
|
764
|
+
result: { config: updated }
|
|
765
|
+
});
|
|
766
|
+
} catch (e) {
|
|
767
|
+
sendResponse(socket, {
|
|
768
|
+
id,
|
|
769
|
+
ok: false,
|
|
770
|
+
error: e instanceof Error ? e.message : String(e)
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
socket.end();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (method === "notification_send") {
|
|
777
|
+
const p = params;
|
|
778
|
+
if (!p.message) {
|
|
779
|
+
sendResponse(socket, {
|
|
780
|
+
id,
|
|
781
|
+
ok: false,
|
|
782
|
+
error: "notification_send: message is required"
|
|
783
|
+
});
|
|
784
|
+
socket.end();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
routeNotification({
|
|
788
|
+
event: p.event ?? "info",
|
|
789
|
+
message: p.message,
|
|
790
|
+
title: p.title
|
|
791
|
+
}, notificationConfig).then((result) => {
|
|
792
|
+
sendResponse(socket, {
|
|
793
|
+
id,
|
|
794
|
+
ok: true,
|
|
795
|
+
result
|
|
796
|
+
});
|
|
797
|
+
socket.end();
|
|
798
|
+
}).catch((e) => {
|
|
799
|
+
sendResponse(socket, {
|
|
800
|
+
id,
|
|
801
|
+
ok: false,
|
|
802
|
+
error: e instanceof Error ? e.message : String(e)
|
|
803
|
+
});
|
|
804
|
+
socket.end();
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (method === "observation_store") {
|
|
809
|
+
const pool = storageBackend.getPool?.();
|
|
810
|
+
if (!pool) {
|
|
811
|
+
sendResponse(socket, {
|
|
812
|
+
id,
|
|
813
|
+
ok: false,
|
|
814
|
+
error: "Observations require Postgres backend"
|
|
815
|
+
});
|
|
816
|
+
socket.end();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const p = params;
|
|
821
|
+
let project_id = null;
|
|
822
|
+
let project_slug = null;
|
|
823
|
+
if (p.cwd) {
|
|
824
|
+
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);
|
|
825
|
+
if (row) {
|
|
826
|
+
project_id = row.id;
|
|
827
|
+
project_slug = row.slug;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
await ensureObservationTables(pool);
|
|
831
|
+
sendResponse(socket, {
|
|
832
|
+
id,
|
|
833
|
+
ok: true,
|
|
834
|
+
result: {
|
|
835
|
+
ok: true,
|
|
836
|
+
id: await storeObservation(pool, {
|
|
837
|
+
session_id: p.session_id,
|
|
838
|
+
project_id,
|
|
839
|
+
project_slug,
|
|
840
|
+
type: p.type,
|
|
841
|
+
title: p.title,
|
|
842
|
+
narrative: p.narrative ?? null,
|
|
843
|
+
tool_name: p.tool_name,
|
|
844
|
+
tool_input_summary: p.tool_input_summary ?? null,
|
|
845
|
+
files_read: p.files_read ?? [],
|
|
846
|
+
files_modified: p.files_modified ?? [],
|
|
847
|
+
concepts: p.concepts ?? []
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
} catch (e) {
|
|
852
|
+
sendResponse(socket, {
|
|
853
|
+
id,
|
|
854
|
+
ok: false,
|
|
855
|
+
error: e instanceof Error ? e.message : String(e)
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
socket.end();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (method === "skill_telemetry_record") {
|
|
862
|
+
const pool = storageBackend.getPool?.();
|
|
863
|
+
if (!pool) {
|
|
864
|
+
sendResponse(socket, {
|
|
865
|
+
id,
|
|
866
|
+
ok: false,
|
|
867
|
+
error: "Skill telemetry requires Postgres backend"
|
|
868
|
+
});
|
|
869
|
+
socket.end();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
const p = params;
|
|
874
|
+
let project_slug = null;
|
|
875
|
+
if (p.cwd) {
|
|
876
|
+
const row = registryDb.prepare("SELECT slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1").get(p.cwd);
|
|
877
|
+
if (row) project_slug = row.slug;
|
|
878
|
+
}
|
|
879
|
+
await recordSkillInvocation(pool, {
|
|
880
|
+
skill_name: p.skill_name,
|
|
881
|
+
source: p.source,
|
|
882
|
+
scope: p.scope,
|
|
883
|
+
project_slug
|
|
884
|
+
});
|
|
885
|
+
sendResponse(socket, {
|
|
886
|
+
id,
|
|
887
|
+
ok: true,
|
|
888
|
+
result: { ok: true }
|
|
889
|
+
});
|
|
890
|
+
} catch (e) {
|
|
891
|
+
sendResponse(socket, {
|
|
892
|
+
id,
|
|
893
|
+
ok: false,
|
|
894
|
+
error: e instanceof Error ? e.message : String(e)
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
socket.end();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (method === "skill_telemetry_query") {
|
|
901
|
+
const pool = storageBackend.getPool?.();
|
|
902
|
+
if (!pool) {
|
|
903
|
+
sendResponse(socket, {
|
|
904
|
+
id,
|
|
905
|
+
ok: false,
|
|
906
|
+
error: "Skill telemetry requires Postgres backend"
|
|
907
|
+
});
|
|
908
|
+
socket.end();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const p = params;
|
|
913
|
+
sendResponse(socket, {
|
|
914
|
+
id,
|
|
915
|
+
ok: true,
|
|
916
|
+
result: await querySkillTelemetry(pool, {
|
|
917
|
+
scope: p.scope,
|
|
918
|
+
status: p.status,
|
|
919
|
+
limit: p.limit
|
|
920
|
+
})
|
|
921
|
+
});
|
|
922
|
+
} catch (e) {
|
|
923
|
+
sendResponse(socket, {
|
|
924
|
+
id,
|
|
925
|
+
ok: false,
|
|
926
|
+
error: e instanceof Error ? e.message : String(e)
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
socket.end();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (method === "observation_query") {
|
|
933
|
+
const pool = storageBackend.getPool?.();
|
|
934
|
+
if (!pool) {
|
|
935
|
+
sendResponse(socket, {
|
|
936
|
+
id,
|
|
937
|
+
ok: false,
|
|
938
|
+
error: "Observations require Postgres backend"
|
|
939
|
+
});
|
|
940
|
+
socket.end();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const p = params;
|
|
945
|
+
sendResponse(socket, {
|
|
946
|
+
id,
|
|
947
|
+
ok: true,
|
|
948
|
+
result: await queryObservations(pool, {
|
|
949
|
+
projectId: p.project_id,
|
|
950
|
+
sessionId: p.session_id,
|
|
951
|
+
type: p.type,
|
|
952
|
+
limit: p.limit,
|
|
953
|
+
offset: p.offset
|
|
954
|
+
})
|
|
955
|
+
});
|
|
956
|
+
} catch (e) {
|
|
957
|
+
sendResponse(socket, {
|
|
958
|
+
id,
|
|
959
|
+
ok: false,
|
|
960
|
+
error: e instanceof Error ? e.message : String(e)
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
socket.end();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (method === "observation_recent") {
|
|
967
|
+
const pool = storageBackend.getPool?.();
|
|
968
|
+
if (!pool) {
|
|
969
|
+
sendResponse(socket, {
|
|
970
|
+
id,
|
|
971
|
+
ok: false,
|
|
972
|
+
error: "Observations require Postgres backend"
|
|
973
|
+
});
|
|
974
|
+
socket.end();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
const p = params;
|
|
979
|
+
const limit = p.limit ?? 20;
|
|
980
|
+
let resolvedProjectId = p.project_id;
|
|
981
|
+
let resolvedProjectSlug;
|
|
982
|
+
if (resolvedProjectId === void 0 && p.cwd) {
|
|
983
|
+
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);
|
|
984
|
+
if (row) {
|
|
985
|
+
resolvedProjectId = row.id;
|
|
986
|
+
resolvedProjectSlug = row.slug;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
let rows;
|
|
990
|
+
if (resolvedProjectId !== void 0) rows = await queryRecentObservations(pool, resolvedProjectId, limit);
|
|
991
|
+
else rows = await queryObservations(pool, { limit });
|
|
992
|
+
sendResponse(socket, {
|
|
993
|
+
id,
|
|
994
|
+
ok: true,
|
|
995
|
+
result: {
|
|
996
|
+
rows,
|
|
997
|
+
project_slug: resolvedProjectSlug
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
sendResponse(socket, {
|
|
1002
|
+
id,
|
|
1003
|
+
ok: false,
|
|
1004
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
socket.end();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (method === "observation_list") {
|
|
1011
|
+
const pool = storageBackend.getPool?.();
|
|
1012
|
+
if (!pool) {
|
|
1013
|
+
sendResponse(socket, {
|
|
1014
|
+
id,
|
|
1015
|
+
ok: false,
|
|
1016
|
+
error: "Observations require Postgres backend"
|
|
1017
|
+
});
|
|
1018
|
+
socket.end();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const p = params;
|
|
1023
|
+
let projectId;
|
|
1024
|
+
if (p.project_slug) projectId = registryDb.prepare("SELECT id FROM projects WHERE slug = ?").get(p.project_slug)?.id;
|
|
1025
|
+
sendResponse(socket, {
|
|
1026
|
+
id,
|
|
1027
|
+
ok: true,
|
|
1028
|
+
result: await queryObservations(pool, {
|
|
1029
|
+
projectId,
|
|
1030
|
+
sessionId: p.session_id,
|
|
1031
|
+
type: p.type,
|
|
1032
|
+
limit: p.limit ?? 20,
|
|
1033
|
+
offset: p.offset ?? 0
|
|
1034
|
+
})
|
|
1035
|
+
});
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
sendResponse(socket, {
|
|
1038
|
+
id,
|
|
1039
|
+
ok: false,
|
|
1040
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
socket.end();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (method === "observation_stats") {
|
|
1047
|
+
const pool = storageBackend.getPool?.();
|
|
1048
|
+
if (!pool) {
|
|
1049
|
+
sendResponse(socket, {
|
|
1050
|
+
id,
|
|
1051
|
+
ok: false,
|
|
1052
|
+
error: "Observations require Postgres backend"
|
|
1053
|
+
});
|
|
1054
|
+
socket.end();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
await ensureObservationTables(pool);
|
|
1059
|
+
const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([
|
|
1060
|
+
pool.query("SELECT COUNT(*) as count FROM pai_observations"),
|
|
1061
|
+
pool.query("SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC"),
|
|
1062
|
+
pool.query("SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15"),
|
|
1063
|
+
pool.query("SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1")
|
|
1064
|
+
]);
|
|
1065
|
+
sendResponse(socket, {
|
|
1066
|
+
id,
|
|
1067
|
+
ok: true,
|
|
1068
|
+
result: {
|
|
1069
|
+
total: parseInt(totalRes.rows[0]?.count ?? "0", 10),
|
|
1070
|
+
by_type: byTypeRes.rows.map((r) => ({
|
|
1071
|
+
type: r.type,
|
|
1072
|
+
count: parseInt(r.count, 10)
|
|
1073
|
+
})),
|
|
1074
|
+
by_project: byProjectRes.rows.map((r) => ({
|
|
1075
|
+
project_slug: r.project_slug,
|
|
1076
|
+
count: parseInt(r.count, 10)
|
|
1077
|
+
})),
|
|
1078
|
+
most_recent: recentRes.rows[0]?.created_at ?? null
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
sendResponse(socket, {
|
|
1083
|
+
id,
|
|
1084
|
+
ok: false,
|
|
1085
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
socket.end();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (method === "session_summary_store") {
|
|
1092
|
+
const pool = storageBackend.getPool?.();
|
|
1093
|
+
if (!pool) {
|
|
1094
|
+
sendResponse(socket, {
|
|
1095
|
+
id,
|
|
1096
|
+
ok: false,
|
|
1097
|
+
error: "Session summaries require Postgres backend"
|
|
1098
|
+
});
|
|
1099
|
+
socket.end();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
const p = params;
|
|
1104
|
+
let resolvedProjectId = p.project_id ?? null;
|
|
1105
|
+
let resolvedProjectSlug = p.project_slug ?? null;
|
|
1106
|
+
if (resolvedProjectId === null && p.cwd) {
|
|
1107
|
+
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);
|
|
1108
|
+
if (row) {
|
|
1109
|
+
resolvedProjectId = row.id;
|
|
1110
|
+
resolvedProjectSlug = row.slug;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
await storeSessionSummary(pool, {
|
|
1114
|
+
session_id: p.session_id,
|
|
1115
|
+
project_id: resolvedProjectId,
|
|
1116
|
+
project_slug: resolvedProjectSlug,
|
|
1117
|
+
request: p.request ?? null,
|
|
1118
|
+
investigated: p.investigated ?? null,
|
|
1119
|
+
learned: p.learned ?? null,
|
|
1120
|
+
completed: p.completed ?? null,
|
|
1121
|
+
next_steps: p.next_steps ?? null,
|
|
1122
|
+
observation_count: p.observation_count ?? 0
|
|
1123
|
+
});
|
|
1124
|
+
sendResponse(socket, {
|
|
1125
|
+
id,
|
|
1126
|
+
ok: true,
|
|
1127
|
+
result: { ok: true }
|
|
1128
|
+
});
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
sendResponse(socket, {
|
|
1131
|
+
id,
|
|
1132
|
+
ok: false,
|
|
1133
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
socket.end();
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (method === "work_queue_enqueue") {
|
|
1140
|
+
try {
|
|
1141
|
+
const p = params;
|
|
1142
|
+
if (!p.type) {
|
|
1143
|
+
sendResponse(socket, {
|
|
1144
|
+
id,
|
|
1145
|
+
ok: false,
|
|
1146
|
+
error: "work_queue_enqueue: type is required"
|
|
1147
|
+
});
|
|
1148
|
+
socket.end();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const item = enqueue({
|
|
1152
|
+
type: p.type,
|
|
1153
|
+
priority: p.priority,
|
|
1154
|
+
payload: p.payload ?? {},
|
|
1155
|
+
maxAttempts: p.maxAttempts
|
|
1156
|
+
});
|
|
1157
|
+
/* @__PURE__ */ notifyNewWork();
|
|
1158
|
+
sendResponse(socket, {
|
|
1159
|
+
id,
|
|
1160
|
+
ok: true,
|
|
1161
|
+
result: {
|
|
1162
|
+
id: item.id,
|
|
1163
|
+
status: item.status
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
sendResponse(socket, {
|
|
1168
|
+
id,
|
|
1169
|
+
ok: false,
|
|
1170
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
socket.end();
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (method === "work_queue_stats") {
|
|
1177
|
+
sendResponse(socket, {
|
|
1178
|
+
id,
|
|
1179
|
+
ok: true,
|
|
1180
|
+
result: getStats()
|
|
1181
|
+
});
|
|
1182
|
+
socket.end();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
sendResponse(socket, {
|
|
1187
|
+
id,
|
|
1188
|
+
ok: true,
|
|
1189
|
+
result: await dispatchTool(method, params)
|
|
1190
|
+
});
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
sendResponse(socket, {
|
|
1193
|
+
id,
|
|
1194
|
+
ok: false,
|
|
1195
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
socket.end();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/daemon/daemon/server.ts
|
|
1203
|
+
/**
|
|
1204
|
+
* IPC server and daemon entry point.
|
|
1205
|
+
* Owns: isSocketLive, startIpcServer, serve (exported).
|
|
1206
|
+
*/
|
|
1207
|
+
/**
|
|
1208
|
+
* Check whether an existing socket file is actually being served by a live process.
|
|
1209
|
+
*/
|
|
1210
|
+
function isSocketLive(path) {
|
|
1211
|
+
return new Promise((resolve) => {
|
|
1212
|
+
const client = connect(path);
|
|
1213
|
+
const timer = setTimeout(() => {
|
|
1214
|
+
client.destroy();
|
|
1215
|
+
resolve(false);
|
|
1216
|
+
}, 500);
|
|
1217
|
+
client.on("connect", () => {
|
|
1218
|
+
clearTimeout(timer);
|
|
1219
|
+
client.end();
|
|
1220
|
+
resolve(true);
|
|
1221
|
+
});
|
|
1222
|
+
client.on("error", () => {
|
|
1223
|
+
clearTimeout(timer);
|
|
1224
|
+
resolve(false);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Start the Unix Domain Socket IPC server.
|
|
1230
|
+
*/
|
|
1231
|
+
async function startIpcServer(socketPath) {
|
|
1232
|
+
if (existsSync(socketPath)) {
|
|
1233
|
+
if (await isSocketLive(socketPath)) throw new Error("Another daemon is already running — socket is live. Aborting startup.");
|
|
1234
|
+
try {
|
|
1235
|
+
unlinkSync(socketPath);
|
|
1236
|
+
process.stderr.write("[pai-daemon] Removed stale socket file.\n");
|
|
1237
|
+
} catch {}
|
|
1238
|
+
}
|
|
1239
|
+
const server = createServer((socket) => {
|
|
1240
|
+
let buffer = "";
|
|
1241
|
+
socket.on("data", (chunk) => {
|
|
1242
|
+
buffer += chunk.toString();
|
|
1243
|
+
let nl;
|
|
1244
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
1245
|
+
const line = buffer.slice(0, nl);
|
|
1246
|
+
buffer = buffer.slice(nl + 1);
|
|
1247
|
+
if (line.trim() === "") continue;
|
|
1248
|
+
let request;
|
|
1249
|
+
try {
|
|
1250
|
+
request = JSON.parse(line);
|
|
1251
|
+
} catch {
|
|
1252
|
+
sendResponse(socket, {
|
|
1253
|
+
id: "?",
|
|
1254
|
+
ok: false,
|
|
1255
|
+
error: "Invalid JSON"
|
|
1256
|
+
});
|
|
1257
|
+
socket.destroy();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
handleRequest(request, socket).catch((e) => {
|
|
1261
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1262
|
+
sendResponse(socket, {
|
|
1263
|
+
id: request.id,
|
|
1264
|
+
ok: false,
|
|
1265
|
+
error: msg
|
|
1266
|
+
});
|
|
1267
|
+
socket.destroy();
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
socket.on("error", () => {});
|
|
1272
|
+
});
|
|
1273
|
+
server.on("error", (e) => {
|
|
1274
|
+
process.stderr.write(`[pai-daemon] IPC server error: ${e}\n`);
|
|
1275
|
+
});
|
|
1276
|
+
server.listen(socketPath, () => {
|
|
1277
|
+
process.stderr.write(`[pai-daemon] IPC server listening on ${socketPath}\n`);
|
|
1278
|
+
});
|
|
1279
|
+
return server;
|
|
1280
|
+
}
|
|
1281
|
+
async function serve(config) {
|
|
1282
|
+
setDaemonConfig(config);
|
|
1283
|
+
setStartTime(Date.now());
|
|
1284
|
+
setNotificationConfig(loadNotificationConfig());
|
|
1285
|
+
process.stderr.write("[pai-daemon] Starting daemon...\n");
|
|
1286
|
+
process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
|
|
1287
|
+
process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
|
|
1288
|
+
const { notificationConfig } = await import("./state-DTvy-jRB.mjs").then((n) => n.D);
|
|
1289
|
+
process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
|
|
1290
|
+
try {
|
|
1291
|
+
setPriority(process.pid, 10);
|
|
1292
|
+
} catch {}
|
|
1293
|
+
configureEmbeddingModel(config.embeddingModel);
|
|
1294
|
+
try {
|
|
1295
|
+
setRegistryDb(openRegistry());
|
|
1296
|
+
process.stderr.write("[pai-daemon] Registry database opened.\n");
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1299
|
+
process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\n`);
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
const server = await startIpcServer(config.socketPath);
|
|
1303
|
+
startRegistryScanScheduler();
|
|
1304
|
+
createStorageBackend(config, { waitForPostgres: true }).then((backend) => {
|
|
1305
|
+
setStorageBackend(backend);
|
|
1306
|
+
process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
|
|
1307
|
+
startIndexScheduler();
|
|
1308
|
+
if (backend.backendType === "postgres") startEmbedScheduler();
|
|
1309
|
+
else process.stderr.write("[pai-daemon] Embed scheduler: disabled (SQLite backend)\n");
|
|
1310
|
+
loadQueue();
|
|
1311
|
+
startWorker();
|
|
1312
|
+
}).catch((e) => {
|
|
1313
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1314
|
+
process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
|
|
1315
|
+
process.exit(1);
|
|
1316
|
+
});
|
|
1317
|
+
const shutdown = async (signal) => {
|
|
1318
|
+
process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
|
|
1319
|
+
setShutdownRequested(true);
|
|
1320
|
+
if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
|
|
1321
|
+
if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
|
|
1322
|
+
stopWorker();
|
|
1323
|
+
server.close();
|
|
1324
|
+
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
1325
|
+
const POLL_INTERVAL_MS = 100;
|
|
1326
|
+
const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;
|
|
1327
|
+
if (indexInProgress || embedInProgress) {
|
|
1328
|
+
process.stderr.write(`[pai-daemon] Waiting for in-progress operations to finish (index=${indexInProgress}, embed=${embedInProgress})...\n`);
|
|
1329
|
+
while ((indexInProgress || embedInProgress) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1330
|
+
if (indexInProgress || embedInProgress) process.stderr.write("[pai-daemon] Shutdown timeout reached — forcing exit.\n");
|
|
1331
|
+
else process.stderr.write("[pai-daemon] In-progress operations finished.\n");
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
await storageBackend.close();
|
|
1335
|
+
} catch {}
|
|
1336
|
+
try {
|
|
1337
|
+
unlinkSync(config.socketPath);
|
|
1338
|
+
} catch {}
|
|
1339
|
+
process.exit(0);
|
|
1340
|
+
};
|
|
1341
|
+
process.on("SIGINT", () => {
|
|
1342
|
+
shutdown("SIGINT").catch(() => process.exit(0));
|
|
1343
|
+
});
|
|
1344
|
+
process.on("SIGTERM", () => {
|
|
1345
|
+
shutdown("SIGTERM").catch(() => process.exit(0));
|
|
1346
|
+
});
|
|
1347
|
+
await new Promise(() => {});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/daemon/daemon.ts
|
|
1352
|
+
var daemon_exports = /* @__PURE__ */ __exportAll({ serve: () => serve });
|
|
1353
|
+
|
|
1354
|
+
//#endregion
|
|
1355
|
+
export { serve as n, daemon_exports as t };
|
|
1356
|
+
//# sourceMappingURL=daemon-BaBTOB1P.mjs.map
|