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