@tekmidian/pai 0.42.0 → 0.42.2
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/README.md +1 -1
- package/dist/chain-B-DbR82m.mjs +164 -0
- package/dist/chain-B-DbR82m.mjs.map +1 -0
- package/dist/cli/index.mjs +8 -8
- package/dist/cli/program.mjs +8 -8
- package/dist/{config-DgzR5sfb.mjs → config-_lQI_W08.mjs} +116 -3
- package/dist/config-_lQI_W08.mjs.map +1 -0
- package/dist/{context-handover-cache-dUnSZwaA.mjs → context-handover-cache-pNFdBNrU.mjs} +27 -8
- package/dist/context-handover-cache-pNFdBNrU.mjs.map +1 -0
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-CKBkhOsZ.mjs → daemon-Awt_3UJp.mjs} +7 -7
- package/dist/{daemon-DsvP8KMy.mjs → daemon-Dq8A1uDF.mjs} +591 -370
- package/dist/daemon-Dq8A1uDF.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +5 -5
- package/dist/{fallback-5UJO-RyX.mjs → fallback-vQPI1qxt.mjs} +15 -13
- package/dist/fallback-vQPI1qxt.mjs.map +1 -0
- package/dist/hooks/mcp-deferred-gate.mjs +286 -0
- package/dist/hooks/mcp-deferred-gate.mjs.map +7 -0
- package/dist/hooks/mcp-deferred-reminder.mjs +54 -0
- package/dist/hooks/mcp-deferred-reminder.mjs.map +7 -0
- package/dist/hooks/route-agents-to-worker.mjs +65 -2
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +64 -1
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +68 -2
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/hooks/worker-supervision.mjs +64 -1
- package/dist/hooks/worker-supervision.mjs.map +2 -2
- package/dist/{main-resolver-DXGuygFG.mjs → main-resolver-BAfOMppW.mjs} +2 -2
- package/dist/{main-resolver-BgIrC-D9.mjs → main-resolver-DnkxPYkr.mjs} +111 -16
- package/dist/main-resolver-DnkxPYkr.mjs.map +1 -0
- package/dist/{planner-BLDJt3ZS.mjs → planner-BfEGcEs1.mjs} +5 -5
- package/dist/{planner-BLDJt3ZS.mjs.map → planner-BfEGcEs1.mjs.map} +1 -1
- package/dist/{program-CruRnLtc.mjs → program-BDmeZP-v.mjs} +17 -11
- package/dist/{program-CruRnLtc.mjs.map → program-BDmeZP-v.mjs.map} +1 -1
- package/dist/{chain-DGXNoTwc.mjs → run-CGXbhf2k.mjs} +422 -195
- package/dist/run-CGXbhf2k.mjs.map +1 -0
- package/dist/{server-Cp7tNzNC.mjs → server-D4vAieF4.mjs} +49 -8
- package/dist/server-D4vAieF4.mjs.map +1 -0
- package/dist/{state-BTa10SOM.mjs → state-BRdB-Bf1.mjs} +7 -2
- package/dist/state-BRdB-Bf1.mjs.map +1 -0
- package/dist/state-CY6I-Hnc.mjs +3 -0
- package/dist/{work-queue-worker-D1p2V-ae.mjs → work-queue-worker-AEmfVE0F.mjs} +5 -5
- package/dist/{work-queue-worker-7Vkli4Yh.mjs → work-queue-worker-DIT-VLSh.mjs} +3 -3
- package/dist/{work-queue-worker-7Vkli4Yh.mjs.map → work-queue-worker-DIT-VLSh.mjs.map} +1 -1
- package/docs/cache-keepalive.md +82 -0
- package/docs/commands/worker.md +1 -0
- package/docs/merge-demo.md +1 -0
- package/docs/provider-abstraction.md +52 -2
- package/package.json +1 -1
- package/plugins/core/hooks/hooks.json +12 -0
- package/src/hooks/ts/lib/mcp-deferred-gate.test.ts +258 -0
- package/src/hooks/ts/lib/mcp-deferred-gate.ts +261 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +90 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.ts +66 -0
- package/src/hooks/ts/pre-tool-use/mcp-deferred-gate.ts +126 -0
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +6 -2
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +52 -0
- package/statusline-command.sh +149 -72
- package/dist/chain-DGXNoTwc.mjs.map +0 -1
- package/dist/config-DgzR5sfb.mjs.map +0 -1
- package/dist/context-handover-cache-dUnSZwaA.mjs.map +0 -1
- package/dist/daemon-DsvP8KMy.mjs.map +0 -1
- package/dist/fallback-5UJO-RyX.mjs.map +0 -1
- package/dist/main-resolver-BgIrC-D9.mjs.map +0 -1
- package/dist/scope-NIwCGxRI.mjs +0 -351
- package/dist/scope-NIwCGxRI.mjs.map +0 -1
- package/dist/server-Cp7tNzNC.mjs.map +0 -1
- package/dist/state-BTa10SOM.mjs.map +0 -1
- package/dist/state-RXepvbnO.mjs +0 -3
|
@@ -5,14 +5,14 @@ import { n as indexAll } from "./sync-BWbe8JTg.mjs";
|
|
|
5
5
|
import { t as configureEmbeddingModel } from "./embeddings-DOLZnT1X.mjs";
|
|
6
6
|
import { l as DEFAULT_NOTIFICATION_CONFIG, n as CONFIG_FILE, t as CONFIG_DIR } from "./config-B64vFg14.mjs";
|
|
7
7
|
import { n as getBackendOutage, t as createStorageBackend } from "./factory-DEb9NmHJ.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import { _ as workersLogDir } from "./server-
|
|
10
|
-
import {
|
|
11
|
-
import { C as
|
|
8
|
+
import { _ as readWorkersSection } from "./config-_lQI_W08.mjs";
|
|
9
|
+
import { _ as workersLogDir, m as ledgerPath, p as eventsPath } from "./server-D4vAieF4.mjs";
|
|
10
|
+
import { A as nowStamp, C as alive, D as loadStatus, E as isChatPane, O as loadStatuses, _ as itermForClaudeSession, j as appendLedger, n as runWorker } from "./run-CGXbhf2k.mjs";
|
|
11
|
+
import { A as vaultIndexInProgress, C as setShutdownRequested, D as shutdownRequested, E as setVaultIndexInProgress, O as startTime, S as setRegistryDb, T as setStorageBackend, _ as setIndexSchedulerTimer, a as indexInProgress, b as setLastVaultIndexTime, c as lastIndexTime, d as registryDb, f as setCacheKeepaliveTimer, g as setIndexInProgress, h as setEmbedSchedulerTimer, i as embedSchedulerTimer, k as storageBackend, l as lastVaultIndexTime, m as setEmbedInProgress, n as daemonConfig, o as indexSchedulerTimer, p as setDaemonConfig, r as embedInProgress, s as lastEmbedTime, t as cacheKeepaliveTimer, u as notificationConfig, v as setLastEmbedTime, w as setStartTime, x as setNotificationConfig, y as setLastIndexTime } from "./state-BRdB-Bf1.mjs";
|
|
12
12
|
import { t as routeNotification } from "./router-DD6D4j31.mjs";
|
|
13
13
|
import { C as toolProjectInfo, D as toolMemoryGet, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, a as toolMemoryWakeup, b as toolSessionRoute, i as toolMemoryTaxonomy, n as toolMemoryFeedback, t as toolMemoryKgSearch, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-LlzcV_eK.mjs";
|
|
14
14
|
import { t as detectTopicShift } from "./detector-C73Nxm94.mjs";
|
|
15
|
-
import { a as enqueue, i as stopWorker, n as notifyNewWork, o as getStats, r as startWorker, s as loadQueue } from "./work-queue-worker-
|
|
15
|
+
import { a as enqueue, i as stopWorker, n as notifyNewWork, o as getStats, r as startWorker, s as loadQueue } from "./work-queue-worker-DIT-VLSh.mjs";
|
|
16
16
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { setPriority } from "node:os";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
@@ -87,6 +87,141 @@ function patchNotificationConfig(patch) {
|
|
|
87
87
|
return current;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/workers/keepalive.ts
|
|
92
|
+
/**
|
|
93
|
+
* keepalive.ts — one cache-keepalive beat, shared by the daemon scheduler and
|
|
94
|
+
* the probe script.
|
|
95
|
+
*
|
|
96
|
+
* The provider (GLM anthropic-compat endpoint) caches the worker prefix
|
|
97
|
+
* implicitly — no cache_control blocks — so a trivial single-turn worker spawn
|
|
98
|
+
* through the very same runWorker path re-arms that cache for the next real
|
|
99
|
+
* worker: identical env, identical tool grants, identical WORKER_CONTRACT_PROMPT
|
|
100
|
+
* by construction, because it IS a worker spawn.
|
|
101
|
+
*
|
|
102
|
+
* Measured 2026-09-18 (docs/cache-keepalive.md): the implicit cache only
|
|
103
|
+
* serves spawns within seconds of the previous identical request and never
|
|
104
|
+
* survived a 60 s beat cadence in the armed proof — hence the knob defaults
|
|
105
|
+
* to off and arming it is the operator's explicit call.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately NOT a hand-built claude invocation: duplicated prefix
|
|
108
|
+
* construction is how this repo has shipped real breakage before (probeResume
|
|
109
|
+
* x3, archiver x2). Everything flows through runWorker.
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* The trivial prompt every cache beat (and probe) carries. Kept in one place
|
|
113
|
+
* so the heartbeat arms exactly the prefix a verifying probe measures.
|
|
114
|
+
*/
|
|
115
|
+
const HEARTBEAT_PROMPT = "Reply with exactly one word: pong.";
|
|
116
|
+
/** The worker class a beat runs as — the cheapest spawn the config defines. */
|
|
117
|
+
const KEEPALIVE_CLASS = "simple";
|
|
118
|
+
/** Worker label of every beat, so `pai worker ps` can tell them apart. */
|
|
119
|
+
const KEEPALIVE_LABEL = "cache-keepalive";
|
|
120
|
+
/** Cadence a keepalive runs at for this config; 0 keeps it off. */
|
|
121
|
+
function keepaliveSecs(workers) {
|
|
122
|
+
return workers.enabled ? workers.cacheKeepaliveSecs : 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The LAST `type:"result"` event of a worker transcript, or null when the
|
|
126
|
+
* file is missing, unreadable, or carries no result line (a failed run can
|
|
127
|
+
* end without one).
|
|
128
|
+
*/
|
|
129
|
+
function parseLastResultEvent(path) {
|
|
130
|
+
let text;
|
|
131
|
+
try {
|
|
132
|
+
text = readFileSync(path, "utf8");
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
let found = null;
|
|
137
|
+
for (const line of text.split("\n")) {
|
|
138
|
+
if (!line.startsWith("{")) continue;
|
|
139
|
+
let e;
|
|
140
|
+
try {
|
|
141
|
+
e = JSON.parse(line);
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (e.type === "result") found = e;
|
|
146
|
+
}
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Metrics of one run from its final result event. Single-turn runs make the
|
|
151
|
+
* cumulative result usage identical to first-turn usage, so a warm cache
|
|
152
|
+
* shows up directly as cache_read_input_tokens > 0.
|
|
153
|
+
*/
|
|
154
|
+
function extractBeatMetrics(m, ev) {
|
|
155
|
+
const u = ev?.usage;
|
|
156
|
+
return {
|
|
157
|
+
...m,
|
|
158
|
+
input_tokens: u?.input_tokens ?? null,
|
|
159
|
+
output_tokens: u?.output_tokens ?? null,
|
|
160
|
+
cache_read_input_tokens: u?.cache_read_input_tokens ?? null,
|
|
161
|
+
cache_creation_input_tokens: u?.cache_creation_input_tokens ?? null,
|
|
162
|
+
duration_api_ms: ev?.duration_api_ms ?? null,
|
|
163
|
+
duration_ms: ev?.duration_ms ?? null,
|
|
164
|
+
num_turns: ev?.num_turns ?? null
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** True while a beat's worker is still running — the overlap guard reads it. */
|
|
168
|
+
let beatInFlight = false;
|
|
169
|
+
/** `keepalive-<YYYYMMDD-HHMMSS>` — beats are minutes apart, seconds suffice. */
|
|
170
|
+
function beatId(d = /* @__PURE__ */ new Date()) {
|
|
171
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
172
|
+
return `keepalive-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* One heartbeat: a single-turn trivial worker through the exact worker spawn
|
|
176
|
+
* path, its result metrics parsed from the transcript, one WORKER-KEEPALIVE
|
|
177
|
+
* ledger line. Returns null when another beat is still running (overlap
|
|
178
|
+
* guard) — a skipped beat is free, a queue of them is not.
|
|
179
|
+
*
|
|
180
|
+
* The beat runs in the workers logDir: no session renders its status line
|
|
181
|
+
* there, so the spawn adopts no spawnerSession and no iTerm identity, and
|
|
182
|
+
* worker supervision has no owner to notify — heartbeats are infrastructure,
|
|
183
|
+
* not somebody's workers.
|
|
184
|
+
*/
|
|
185
|
+
async function runKeepaliveBeat(opts = {}) {
|
|
186
|
+
if (beatInFlight) return null;
|
|
187
|
+
beatInFlight = true;
|
|
188
|
+
try {
|
|
189
|
+
const { workers: config } = readWorkersSection(opts.configPath);
|
|
190
|
+
const logDir = workersLogDir(config);
|
|
191
|
+
const id = opts.id ?? beatId();
|
|
192
|
+
const rc = await runWorker({
|
|
193
|
+
claudeArgs: ["-p", HEARTBEAT_PROMPT],
|
|
194
|
+
className: KEEPALIVE_CLASS,
|
|
195
|
+
label: KEEPALIVE_LABEL,
|
|
196
|
+
noPane: true,
|
|
197
|
+
quiet: true,
|
|
198
|
+
worktreeFlag: false,
|
|
199
|
+
id,
|
|
200
|
+
cwd: logDir
|
|
201
|
+
});
|
|
202
|
+
const status = loadStatus(logDir, id);
|
|
203
|
+
const metrics = extractBeatMetrics({
|
|
204
|
+
id,
|
|
205
|
+
ok: rc === 0,
|
|
206
|
+
rc,
|
|
207
|
+
provider: status?.provider ?? null,
|
|
208
|
+
model: status?.model ?? null
|
|
209
|
+
}, parseLastResultEvent(eventsPath(logDir, id)));
|
|
210
|
+
appendLedger(ledgerPath(logDir), "WORKER-KEEPALIVE", {
|
|
211
|
+
id,
|
|
212
|
+
provider: metrics.provider,
|
|
213
|
+
model: metrics.model,
|
|
214
|
+
input_tokens: metrics.input_tokens,
|
|
215
|
+
cache_read_input_tokens: metrics.cache_read_input_tokens,
|
|
216
|
+
cache_creation_input_tokens: metrics.cache_creation_input_tokens,
|
|
217
|
+
duration_api_ms: metrics.duration_api_ms
|
|
218
|
+
});
|
|
219
|
+
return metrics;
|
|
220
|
+
} finally {
|
|
221
|
+
beatInFlight = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
90
225
|
//#endregion
|
|
91
226
|
//#region src/workers/supervision.ts
|
|
92
227
|
/**
|
|
@@ -102,7 +237,9 @@ function patchNotificationConfig(patch) {
|
|
|
102
237
|
* failed — the run ended badly (state failed/killed/lost, or rc != 0,
|
|
103
238
|
* or the runner pid vanished while state was still running)
|
|
104
239
|
* stalled — state running, runner alive, but no new turn for longer than
|
|
105
|
-
* the stall threshold (PAI_WORKER_STALL_MINUTES, default 10)
|
|
240
|
+
* the stall threshold (PAI_WORKER_STALL_MINUTES, default 10);
|
|
241
|
+
* never emitted for the interactive chat pane (origin "chat"),
|
|
242
|
+
* which idles between sessions by design
|
|
106
243
|
*
|
|
107
244
|
* Delivery, in order of preference, never both counted as required:
|
|
108
245
|
* 1. AIBroker `send_to_session` into the orchestrator's terminal — the
|
|
@@ -204,7 +341,7 @@ function detectSupervisionEvents(statuses, opts) {
|
|
|
204
341
|
else if (!isAlive(s.pid) && ageMs(s.updated, opts.now) > deadGraceMs) {
|
|
205
342
|
kind = "failed";
|
|
206
343
|
rc = null;
|
|
207
|
-
} else if (ageMs(s.updated, opts.now) > opts.stallMs) {
|
|
344
|
+
} else if (!isChatPane(s) && ageMs(s.updated, opts.now) > opts.stallMs) {
|
|
208
345
|
kind = "stalled";
|
|
209
346
|
stalledMin = Math.floor(ageMs(s.updated, opts.now) / 6e4);
|
|
210
347
|
}
|
|
@@ -377,368 +514,132 @@ async function runSupervisionTick(logDir, opts = {}) {
|
|
|
377
514
|
}
|
|
378
515
|
|
|
379
516
|
//#endregion
|
|
380
|
-
//#region src/
|
|
517
|
+
//#region src/observations/store.ts
|
|
381
518
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
519
|
+
* store.ts — PostgreSQL persistence for PAI observations.
|
|
520
|
+
*
|
|
521
|
+
* All functions accept a pg.Pool and are safe to call concurrently.
|
|
522
|
+
* Schema is initialized lazily via ensureObservationTables().
|
|
523
|
+
*
|
|
524
|
+
* Content-hash deduplication: observations with the same hash
|
|
525
|
+
* created within a 30-second window are silently dropped to prevent
|
|
526
|
+
* duplicate entries from rapid repeated tool calls.
|
|
385
527
|
*/
|
|
386
|
-
|
|
387
|
-
const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
|
|
528
|
+
let _tablesEnsured = false;
|
|
388
529
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
530
|
+
* Inlined schema DDL — avoids runtime file reads that break in bundled code
|
|
531
|
+
* (the bundler puts this in a shared chunk whose __dirname differs from src/).
|
|
391
532
|
*/
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
533
|
+
const SCHEMA_SQL = `
|
|
534
|
+
CREATE TABLE IF NOT EXISTS pai_observations (
|
|
535
|
+
id SERIAL PRIMARY KEY,
|
|
536
|
+
session_id TEXT NOT NULL,
|
|
537
|
+
project_id INTEGER,
|
|
538
|
+
project_slug TEXT,
|
|
539
|
+
type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
|
540
|
+
title TEXT NOT NULL,
|
|
541
|
+
narrative TEXT,
|
|
542
|
+
tool_name TEXT,
|
|
543
|
+
tool_input_summary TEXT,
|
|
544
|
+
files_read JSONB DEFAULT '[]'::jsonb,
|
|
545
|
+
files_modified JSONB DEFAULT '[]'::jsonb,
|
|
546
|
+
concepts JSONB DEFAULT '[]'::jsonb,
|
|
547
|
+
content_hash TEXT,
|
|
548
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
|
|
552
|
+
CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
|
|
553
|
+
CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
|
|
554
|
+
CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
|
|
555
|
+
CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
|
|
556
|
+
|
|
557
|
+
CREATE TABLE IF NOT EXISTS pai_session_summaries (
|
|
558
|
+
id SERIAL PRIMARY KEY,
|
|
559
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
560
|
+
project_id INTEGER,
|
|
561
|
+
project_slug TEXT,
|
|
562
|
+
request TEXT,
|
|
563
|
+
investigated TEXT,
|
|
564
|
+
learned TEXT,
|
|
565
|
+
completed TEXT,
|
|
566
|
+
next_steps TEXT,
|
|
567
|
+
observation_count INTEGER DEFAULT 0,
|
|
568
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
|
|
573
|
+
`;
|
|
427
574
|
/**
|
|
428
|
-
* Run
|
|
429
|
-
*
|
|
575
|
+
* Run schema DDL idempotently against the given pool.
|
|
576
|
+
* Uses a module-level flag so subsequent calls are no-ops within the same
|
|
577
|
+
* process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
|
|
430
578
|
*/
|
|
431
|
-
async function
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (indexInProgress || embedInProgress) {
|
|
438
|
-
process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const { lastVaultIndexTime } = await import("./state-RXepvbnO.mjs");
|
|
442
|
-
if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
|
|
443
|
-
let vaultProjectId = daemonConfig.vaultProjectId;
|
|
444
|
-
if (!vaultProjectId) {
|
|
445
|
-
const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
|
|
446
|
-
vaultProjectId = row?.id ?? 999;
|
|
447
|
-
if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
|
|
448
|
-
}
|
|
449
|
-
setVaultIndexInProgress(true);
|
|
450
|
-
const t0 = Date.now();
|
|
451
|
-
process.stderr.write("[pai-daemon] Starting vault index run...\n");
|
|
452
|
-
try {
|
|
453
|
-
const { indexVault } = await import("./vault-indexer-mata9S7b.mjs");
|
|
454
|
-
const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
|
|
455
|
-
const elapsed = Date.now() - t0;
|
|
456
|
-
setLastVaultIndexTime(Date.now());
|
|
457
|
-
process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
|
|
458
|
-
} catch (e) {
|
|
459
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
460
|
-
process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
|
|
461
|
-
} finally {
|
|
462
|
-
setVaultIndexInProgress(false);
|
|
463
|
-
}
|
|
579
|
+
async function ensureObservationTables(pool) {
|
|
580
|
+
if (_tablesEnsured) return;
|
|
581
|
+
await pool.query(SCHEMA_SQL);
|
|
582
|
+
_tablesEnsured = true;
|
|
464
583
|
}
|
|
465
584
|
/**
|
|
466
|
-
*
|
|
585
|
+
* Compute a 16-character hex content hash for deduplication.
|
|
586
|
+
* Hash = SHA256(session_id + tool_name + title).slice(0, 16)
|
|
467
587
|
*/
|
|
468
|
-
function
|
|
469
|
-
|
|
470
|
-
process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
|
|
471
|
-
const cycle = (label, withEmbed) => runIndex().then(() => runVaultIndex()).then(() => withEmbed ? runEmbed() : void 0).catch((e) => {
|
|
472
|
-
process.stderr.write(`[pai-daemon] ${label} index error: ${e}\n`);
|
|
473
|
-
});
|
|
474
|
-
if (daemonConfig.maintenanceHour === void 0) setTimeout(() => void cycle("Startup", daemonConfig.embedOnStartup), 2e3);
|
|
475
|
-
else process.stderr.write("[pai-daemon] Startup index pass skipped (maintenanceHour is set).\n");
|
|
476
|
-
const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);
|
|
477
|
-
if (daemonConfig.maintenanceHour !== void 0) process.stderr.write(`[pai-daemon] Index scheduler anchored to ${String(daemonConfig.maintenanceHour).padStart(2, "0")}:00 local (next pass in ${Math.round(firstDelayMs / 6e4)} min)\n`);
|
|
478
|
-
let timer;
|
|
479
|
-
const startTimer = () => {
|
|
480
|
-
timer = setInterval(() => void cycle("Scheduled", true), intervalMs);
|
|
481
|
-
if (timer.unref) timer.unref();
|
|
482
|
-
setIndexSchedulerTimer(timer);
|
|
483
|
-
};
|
|
484
|
-
const first = setTimeout(() => {
|
|
485
|
-
cycle("Scheduled", true);
|
|
486
|
-
startTimer();
|
|
487
|
-
}, firstDelayMs);
|
|
488
|
-
if (first.unref) first.unref();
|
|
588
|
+
function computeContentHash(sessionId, toolName, title) {
|
|
589
|
+
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
489
590
|
}
|
|
490
591
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
592
|
+
* Insert an observation, skipping duplicates within a 30-second window.
|
|
593
|
+
* Returns the inserted row's id, or null if the insert was suppressed.
|
|
493
594
|
*/
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
595
|
+
async function storeObservation(pool, obs) {
|
|
596
|
+
await ensureObservationTables(pool);
|
|
597
|
+
const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
|
|
598
|
+
const dupCheck = await pool.query(`SELECT id FROM pai_observations
|
|
599
|
+
WHERE content_hash = $1
|
|
600
|
+
AND session_id = $2
|
|
601
|
+
AND created_at >= NOW() - INTERVAL '30 seconds'
|
|
602
|
+
LIMIT 1`, [hash, obs.session_id]);
|
|
603
|
+
if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
|
|
604
|
+
return (await pool.query(`INSERT INTO pai_observations
|
|
605
|
+
(session_id, project_id, project_slug, type, title, narrative,
|
|
606
|
+
tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
|
|
607
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
|
|
608
|
+
RETURNING id`, [
|
|
609
|
+
obs.session_id,
|
|
610
|
+
obs.project_id ?? null,
|
|
611
|
+
obs.project_slug ?? null,
|
|
612
|
+
obs.type,
|
|
613
|
+
obs.title,
|
|
614
|
+
obs.narrative ?? null,
|
|
615
|
+
obs.tool_name,
|
|
616
|
+
obs.tool_input_summary ?? null,
|
|
617
|
+
JSON.stringify(obs.files_read),
|
|
618
|
+
JSON.stringify(obs.files_modified),
|
|
619
|
+
JSON.stringify(obs.concepts),
|
|
620
|
+
hash
|
|
621
|
+
])).rows[0]?.id ?? null;
|
|
501
622
|
}
|
|
502
623
|
/**
|
|
503
|
-
*
|
|
624
|
+
* Attribute an observation to the active project covering `cwd`, then store
|
|
625
|
+
* it. One shared sequence for every daemon-side writer (IPC handler, cache
|
|
626
|
+
* keepalive) so the lookup query exists exactly once.
|
|
504
627
|
*/
|
|
505
|
-
async function
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
628
|
+
async function storeObservationWithProject(db, pool, obs) {
|
|
629
|
+
let project_id = null;
|
|
630
|
+
let project_slug = null;
|
|
631
|
+
if (obs.cwd) {
|
|
632
|
+
const row = db.prepare("SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1").get(obs.cwd);
|
|
633
|
+
if (row) {
|
|
634
|
+
project_id = row.id;
|
|
635
|
+
project_slug = row.slug;
|
|
636
|
+
}
|
|
513
637
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const projectNames = /* @__PURE__ */ new Map();
|
|
520
|
-
try {
|
|
521
|
-
const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
|
|
522
|
-
for (const r of rows) projectNames.set(r.id, r.slug);
|
|
523
|
-
} catch {}
|
|
524
|
-
const { embedChunksWithBackend } = await import("./indexer-backend-BPTzKFZU.mjs");
|
|
525
|
-
const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames, { maxMillis: 24e4 });
|
|
526
|
-
let vaultEmbedCount = 0;
|
|
527
|
-
if (daemonConfig.vaultPath) try {
|
|
528
|
-
const { SQLiteBackend } = await import("./sqlite-Cq3gJcq9.mjs");
|
|
529
|
-
const { openFederation } = await import("./db-O-cyAPfS.mjs");
|
|
530
|
-
const federationDb = openFederation();
|
|
531
|
-
const vaultSqliteBackend = new SQLiteBackend(federationDb);
|
|
532
|
-
const vaultProjectNames = new Map(projectNames);
|
|
533
|
-
if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
|
|
534
|
-
vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames, { maxMillis: 45e3 });
|
|
535
|
-
try {
|
|
536
|
-
federationDb.close();
|
|
537
|
-
} catch {}
|
|
538
|
-
if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
|
|
539
|
-
} catch (ve) {
|
|
540
|
-
const vmsg = ve instanceof Error ? ve.message : String(ve);
|
|
541
|
-
process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
|
|
542
|
-
}
|
|
543
|
-
const elapsed = Date.now() - t0;
|
|
544
|
-
setLastEmbedTime(Date.now());
|
|
545
|
-
process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
|
|
546
|
-
} catch (e) {
|
|
547
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
548
|
-
process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
|
|
549
|
-
} finally {
|
|
550
|
-
setEmbedInProgress(false);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Start the periodic embed scheduler. Initial run is 60 seconds after startup.
|
|
555
|
-
*/
|
|
556
|
-
function startEmbedScheduler() {
|
|
557
|
-
const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
|
|
558
|
-
process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
|
|
559
|
-
if (daemonConfig.embedOnStartup) setTimeout(() => {
|
|
560
|
-
runEmbed().catch((e) => {
|
|
561
|
-
process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
|
|
562
|
-
});
|
|
563
|
-
}, 6e4);
|
|
564
|
-
else process.stderr.write("[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\n");
|
|
565
|
-
if (daemonConfig.maintenanceHour !== void 0) {
|
|
566
|
-
process.stderr.write("[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; embed runs at the end of each anchored index cycle).\n");
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const timer = setInterval(() => {
|
|
570
|
-
runEmbed().catch((e) => {
|
|
571
|
-
process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
|
|
572
|
-
});
|
|
573
|
-
}, intervalMs);
|
|
574
|
-
if (timer.unref) timer.unref();
|
|
575
|
-
setEmbedSchedulerTimer(timer);
|
|
576
|
-
}
|
|
577
|
-
const REGISTRY_SCAN_INTERVAL_MS = 1800 * 1e3;
|
|
578
|
-
const REGISTRY_SCAN_STARTUP_DELAY_MS = 1e4;
|
|
579
|
-
/**
|
|
580
|
-
* Start the periodic registry scan scheduler.
|
|
581
|
-
* First scan runs 10 seconds after startup (enough time for IPC to stabilise).
|
|
582
|
-
* Subsequent scans run every 30 minutes.
|
|
583
|
-
* Uses the work-queue so the scan runs on the worker thread without blocking IPC.
|
|
584
|
-
*/
|
|
585
|
-
function startRegistryScanScheduler() {
|
|
586
|
-
process.stderr.write("[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\n");
|
|
587
|
-
setTimeout(() => {
|
|
588
|
-
import("./work-queue-worker-D1p2V-ae.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
589
|
-
process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\n`);
|
|
590
|
-
});
|
|
591
|
-
}, REGISTRY_SCAN_STARTUP_DELAY_MS);
|
|
592
|
-
const timer = setInterval(() => {
|
|
593
|
-
import("./work-queue-worker-D1p2V-ae.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
594
|
-
process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\n`);
|
|
595
|
-
});
|
|
596
|
-
}, REGISTRY_SCAN_INTERVAL_MS);
|
|
597
|
-
if (timer.unref) timer.unref();
|
|
598
|
-
}
|
|
599
|
-
/** How often the daemon looks at the worker ledger for supervision events. */
|
|
600
|
-
const SUPERVISION_TICK_MS = 3e4;
|
|
601
|
-
/** First supervision pass waits for the first statuses to exist at all. */
|
|
602
|
-
const SUPERVISION_STARTUP_DELAY_MS = 15e3;
|
|
603
|
-
/**
|
|
604
|
-
* Start the worker supervisor: one tick every 30 s over the workers ledger
|
|
605
|
-
* (src/workers/supervision.ts) that pushes finished/failed/stalled events to
|
|
606
|
-
* the owning session — the daemon-side replacement for orchestrator polling.
|
|
607
|
-
* Daemon code only: no model call has any business being here, and none is
|
|
608
|
-
* made. Disabled workers disable the supervisor with it.
|
|
609
|
-
*/
|
|
610
|
-
function startWorkerSupervisor() {
|
|
611
|
-
let enabled = false;
|
|
612
|
-
try {
|
|
613
|
-
enabled = readWorkersSection().workers.enabled;
|
|
614
|
-
} catch (e) {
|
|
615
|
-
process.stderr.write(`[pai-daemon] Worker supervisor: not started (${e instanceof Error ? e.message : String(e)})\n`);
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (!enabled) {
|
|
619
|
-
process.stderr.write("[pai-daemon] Worker supervisor: disabled (workers are off)\n");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const tick = () => {
|
|
623
|
-
runSupervisionTick(workersLogDir(readWorkersSection().workers), { stallMs: stallMinutesFromEnv() * 6e4 }).then((r) => {
|
|
624
|
-
for (const ev of r.events) process.stderr.write(`[pai-daemon] Supervision: ${ev.text}\n`);
|
|
625
|
-
}).catch((e) => {
|
|
626
|
-
process.stderr.write(`[pai-daemon] Supervision tick error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
627
|
-
});
|
|
628
|
-
};
|
|
629
|
-
const first = setTimeout(tick, SUPERVISION_STARTUP_DELAY_MS);
|
|
630
|
-
if (first.unref) first.unref();
|
|
631
|
-
const timer = setInterval(tick, SUPERVISION_TICK_MS);
|
|
632
|
-
if (timer.unref) timer.unref();
|
|
633
|
-
process.stderr.write(`[pai-daemon] Worker supervisor: every ${Math.round(SUPERVISION_TICK_MS / 1e3)}s\n`);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
//#endregion
|
|
637
|
-
//#region src/observations/store.ts
|
|
638
|
-
/**
|
|
639
|
-
* store.ts — PostgreSQL persistence for PAI observations.
|
|
640
|
-
*
|
|
641
|
-
* All functions accept a pg.Pool and are safe to call concurrently.
|
|
642
|
-
* Schema is initialized lazily via ensureObservationTables().
|
|
643
|
-
*
|
|
644
|
-
* Content-hash deduplication: observations with the same hash
|
|
645
|
-
* created within a 30-second window are silently dropped to prevent
|
|
646
|
-
* duplicate entries from rapid repeated tool calls.
|
|
647
|
-
*/
|
|
648
|
-
let _tablesEnsured = false;
|
|
649
|
-
/**
|
|
650
|
-
* Inlined schema DDL — avoids runtime file reads that break in bundled code
|
|
651
|
-
* (the bundler puts this in a shared chunk whose __dirname differs from src/).
|
|
652
|
-
*/
|
|
653
|
-
const SCHEMA_SQL = `
|
|
654
|
-
CREATE TABLE IF NOT EXISTS pai_observations (
|
|
655
|
-
id SERIAL PRIMARY KEY,
|
|
656
|
-
session_id TEXT NOT NULL,
|
|
657
|
-
project_id INTEGER,
|
|
658
|
-
project_slug TEXT,
|
|
659
|
-
type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
|
660
|
-
title TEXT NOT NULL,
|
|
661
|
-
narrative TEXT,
|
|
662
|
-
tool_name TEXT,
|
|
663
|
-
tool_input_summary TEXT,
|
|
664
|
-
files_read JSONB DEFAULT '[]'::jsonb,
|
|
665
|
-
files_modified JSONB DEFAULT '[]'::jsonb,
|
|
666
|
-
concepts JSONB DEFAULT '[]'::jsonb,
|
|
667
|
-
content_hash TEXT,
|
|
668
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
669
|
-
);
|
|
670
|
-
|
|
671
|
-
CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
|
|
672
|
-
CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
|
|
673
|
-
CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
|
|
674
|
-
CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
|
|
675
|
-
CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
|
|
676
|
-
|
|
677
|
-
CREATE TABLE IF NOT EXISTS pai_session_summaries (
|
|
678
|
-
id SERIAL PRIMARY KEY,
|
|
679
|
-
session_id TEXT NOT NULL UNIQUE,
|
|
680
|
-
project_id INTEGER,
|
|
681
|
-
project_slug TEXT,
|
|
682
|
-
request TEXT,
|
|
683
|
-
investigated TEXT,
|
|
684
|
-
learned TEXT,
|
|
685
|
-
completed TEXT,
|
|
686
|
-
next_steps TEXT,
|
|
687
|
-
observation_count INTEGER DEFAULT 0,
|
|
688
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
689
|
-
);
|
|
690
|
-
|
|
691
|
-
CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
|
|
692
|
-
CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
|
|
693
|
-
`;
|
|
694
|
-
/**
|
|
695
|
-
* Run schema DDL idempotently against the given pool.
|
|
696
|
-
* Uses a module-level flag so subsequent calls are no-ops within the same
|
|
697
|
-
* process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
|
|
698
|
-
*/
|
|
699
|
-
async function ensureObservationTables(pool) {
|
|
700
|
-
if (_tablesEnsured) return;
|
|
701
|
-
await pool.query(SCHEMA_SQL);
|
|
702
|
-
_tablesEnsured = true;
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Compute a 16-character hex content hash for deduplication.
|
|
706
|
-
* Hash = SHA256(session_id + tool_name + title).slice(0, 16)
|
|
707
|
-
*/
|
|
708
|
-
function computeContentHash(sessionId, toolName, title) {
|
|
709
|
-
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Insert an observation, skipping duplicates within a 30-second window.
|
|
713
|
-
* Returns the inserted row's id, or null if the insert was suppressed.
|
|
714
|
-
*/
|
|
715
|
-
async function storeObservation(pool, obs) {
|
|
716
|
-
await ensureObservationTables(pool);
|
|
717
|
-
const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
|
|
718
|
-
const dupCheck = await pool.query(`SELECT id FROM pai_observations
|
|
719
|
-
WHERE content_hash = $1
|
|
720
|
-
AND session_id = $2
|
|
721
|
-
AND created_at >= NOW() - INTERVAL '30 seconds'
|
|
722
|
-
LIMIT 1`, [hash, obs.session_id]);
|
|
723
|
-
if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
|
|
724
|
-
return (await pool.query(`INSERT INTO pai_observations
|
|
725
|
-
(session_id, project_id, project_slug, type, title, narrative,
|
|
726
|
-
tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
|
|
727
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
|
|
728
|
-
RETURNING id`, [
|
|
729
|
-
obs.session_id,
|
|
730
|
-
obs.project_id ?? null,
|
|
731
|
-
obs.project_slug ?? null,
|
|
732
|
-
obs.type,
|
|
733
|
-
obs.title,
|
|
734
|
-
obs.narrative ?? null,
|
|
735
|
-
obs.tool_name,
|
|
736
|
-
obs.tool_input_summary ?? null,
|
|
737
|
-
JSON.stringify(obs.files_read),
|
|
738
|
-
JSON.stringify(obs.files_modified),
|
|
739
|
-
JSON.stringify(obs.concepts),
|
|
740
|
-
hash
|
|
741
|
-
])).rows[0]?.id ?? null;
|
|
638
|
+
return storeObservation(pool, {
|
|
639
|
+
...obs,
|
|
640
|
+
project_id,
|
|
641
|
+
project_slug
|
|
642
|
+
});
|
|
742
643
|
}
|
|
743
644
|
/**
|
|
744
645
|
* Filtered query for observations with optional projectId, sessionId, type,
|
|
@@ -903,6 +804,335 @@ async function querySkillTelemetry(pool, opts = {}) {
|
|
|
903
804
|
LIMIT $${idx}`, params)).rows;
|
|
904
805
|
}
|
|
905
806
|
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/daemon/daemon/scheduler.ts
|
|
809
|
+
/**
|
|
810
|
+
* Index, embed, vault index, and registry scan schedulers for the PAI daemon.
|
|
811
|
+
* Exports run* functions (also called on-demand by the IPC handler)
|
|
812
|
+
* and the start* functions invoked once at daemon startup.
|
|
813
|
+
*/
|
|
814
|
+
/** Minimum interval between vault index runs (30 minutes). */
|
|
815
|
+
const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
|
|
816
|
+
/**
|
|
817
|
+
* Run a full index pass. Guards against overlapping runs with indexInProgress.
|
|
818
|
+
* Called both by the scheduler and by the index_now IPC method.
|
|
819
|
+
*/
|
|
820
|
+
async function runIndex() {
|
|
821
|
+
if (indexInProgress) {
|
|
822
|
+
process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (embedInProgress) {
|
|
826
|
+
process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
setIndexInProgress(true);
|
|
830
|
+
const t0 = Date.now();
|
|
831
|
+
try {
|
|
832
|
+
process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
|
|
833
|
+
if (storageBackend.backendType === "sqlite") {
|
|
834
|
+
const { SQLiteBackend } = await import("./sqlite-Cq3gJcq9.mjs");
|
|
835
|
+
if (storageBackend instanceof SQLiteBackend) {
|
|
836
|
+
const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
|
|
837
|
+
const elapsed = Date.now() - t0;
|
|
838
|
+
setLastIndexTime(Date.now());
|
|
839
|
+
process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
const { indexAllWithBackend } = await import("./indexer-backend-BPTzKFZU.mjs");
|
|
843
|
+
const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
|
|
844
|
+
const elapsed = Date.now() - t0;
|
|
845
|
+
setLastIndexTime(Date.now());
|
|
846
|
+
process.stderr.write(`[pai-daemon] Index complete (postgres): ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
|
|
847
|
+
}
|
|
848
|
+
} catch (e) {
|
|
849
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
850
|
+
process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
|
|
851
|
+
} finally {
|
|
852
|
+
setIndexInProgress(false);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
|
|
857
|
+
* Skips if no vaultPath is configured, or if project index/embed is in progress.
|
|
858
|
+
*/
|
|
859
|
+
async function runVaultIndex() {
|
|
860
|
+
if (!daemonConfig.vaultPath) return;
|
|
861
|
+
if (vaultIndexInProgress) {
|
|
862
|
+
process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (indexInProgress || embedInProgress) {
|
|
866
|
+
process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const { lastVaultIndexTime } = await import("./state-CY6I-Hnc.mjs");
|
|
870
|
+
if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
|
|
871
|
+
let vaultProjectId = daemonConfig.vaultProjectId;
|
|
872
|
+
if (!vaultProjectId) {
|
|
873
|
+
const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
|
|
874
|
+
vaultProjectId = row?.id ?? 999;
|
|
875
|
+
if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
|
|
876
|
+
}
|
|
877
|
+
setVaultIndexInProgress(true);
|
|
878
|
+
const t0 = Date.now();
|
|
879
|
+
process.stderr.write("[pai-daemon] Starting vault index run...\n");
|
|
880
|
+
try {
|
|
881
|
+
const { indexVault } = await import("./vault-indexer-mata9S7b.mjs");
|
|
882
|
+
const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
|
|
883
|
+
const elapsed = Date.now() - t0;
|
|
884
|
+
setLastVaultIndexTime(Date.now());
|
|
885
|
+
process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
|
|
886
|
+
} catch (e) {
|
|
887
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
888
|
+
process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
|
|
889
|
+
} finally {
|
|
890
|
+
setVaultIndexInProgress(false);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.
|
|
895
|
+
*/
|
|
896
|
+
function startIndexScheduler() {
|
|
897
|
+
const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
|
|
898
|
+
process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
|
|
899
|
+
const cycle = (label, withEmbed) => runIndex().then(() => runVaultIndex()).then(() => withEmbed ? runEmbed() : void 0).catch((e) => {
|
|
900
|
+
process.stderr.write(`[pai-daemon] ${label} index error: ${e}\n`);
|
|
901
|
+
});
|
|
902
|
+
if (daemonConfig.maintenanceHour === void 0) setTimeout(() => void cycle("Startup", daemonConfig.embedOnStartup), 2e3);
|
|
903
|
+
else process.stderr.write("[pai-daemon] Startup index pass skipped (maintenanceHour is set).\n");
|
|
904
|
+
const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);
|
|
905
|
+
if (daemonConfig.maintenanceHour !== void 0) process.stderr.write(`[pai-daemon] Index scheduler anchored to ${String(daemonConfig.maintenanceHour).padStart(2, "0")}:00 local (next pass in ${Math.round(firstDelayMs / 6e4)} min)\n`);
|
|
906
|
+
let timer;
|
|
907
|
+
const startTimer = () => {
|
|
908
|
+
timer = setInterval(() => void cycle("Scheduled", true), intervalMs);
|
|
909
|
+
if (timer.unref) timer.unref();
|
|
910
|
+
setIndexSchedulerTimer(timer);
|
|
911
|
+
};
|
|
912
|
+
const first = setTimeout(() => {
|
|
913
|
+
cycle("Scheduled", true);
|
|
914
|
+
startTimer();
|
|
915
|
+
}, firstDelayMs);
|
|
916
|
+
if (first.unref) first.unref();
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Milliseconds until the next occurrence of `hour`:00 local time.
|
|
920
|
+
* Falls back to the plain interval when no anchor hour is configured.
|
|
921
|
+
*/
|
|
922
|
+
function msUntilNextAnchor(hour, intervalMs) {
|
|
923
|
+
if (hour === void 0 || !Number.isInteger(hour) || hour < 0 || hour > 23) return intervalMs;
|
|
924
|
+
const now = /* @__PURE__ */ new Date();
|
|
925
|
+
const next = new Date(now);
|
|
926
|
+
next.setHours(hour, 0, 0, 0);
|
|
927
|
+
if (next <= now) next.setDate(next.getDate() + 1);
|
|
928
|
+
return next.getTime() - now.getTime();
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Run an embedding pass for all unembedded chunks (Postgres backend only).
|
|
932
|
+
*/
|
|
933
|
+
async function runEmbed() {
|
|
934
|
+
if (embedInProgress) {
|
|
935
|
+
process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (indexInProgress) {
|
|
939
|
+
process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (storageBackend.backendType !== "postgres") return;
|
|
943
|
+
setEmbedInProgress(true);
|
|
944
|
+
const t0 = Date.now();
|
|
945
|
+
try {
|
|
946
|
+
process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
|
|
947
|
+
const projectNames = /* @__PURE__ */ new Map();
|
|
948
|
+
try {
|
|
949
|
+
const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
|
|
950
|
+
for (const r of rows) projectNames.set(r.id, r.slug);
|
|
951
|
+
} catch {}
|
|
952
|
+
const { embedChunksWithBackend } = await import("./indexer-backend-BPTzKFZU.mjs");
|
|
953
|
+
const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames, { maxMillis: 24e4 });
|
|
954
|
+
let vaultEmbedCount = 0;
|
|
955
|
+
if (daemonConfig.vaultPath) try {
|
|
956
|
+
const { SQLiteBackend } = await import("./sqlite-Cq3gJcq9.mjs");
|
|
957
|
+
const { openFederation } = await import("./db-O-cyAPfS.mjs");
|
|
958
|
+
const federationDb = openFederation();
|
|
959
|
+
const vaultSqliteBackend = new SQLiteBackend(federationDb);
|
|
960
|
+
const vaultProjectNames = new Map(projectNames);
|
|
961
|
+
if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
|
|
962
|
+
vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames, { maxMillis: 45e3 });
|
|
963
|
+
try {
|
|
964
|
+
federationDb.close();
|
|
965
|
+
} catch {}
|
|
966
|
+
if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
|
|
967
|
+
} catch (ve) {
|
|
968
|
+
const vmsg = ve instanceof Error ? ve.message : String(ve);
|
|
969
|
+
process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
|
|
970
|
+
}
|
|
971
|
+
const elapsed = Date.now() - t0;
|
|
972
|
+
setLastEmbedTime(Date.now());
|
|
973
|
+
process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
|
|
974
|
+
} catch (e) {
|
|
975
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
976
|
+
process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
|
|
977
|
+
} finally {
|
|
978
|
+
setEmbedInProgress(false);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Start the periodic embed scheduler. Initial run is 60 seconds after startup.
|
|
983
|
+
*/
|
|
984
|
+
function startEmbedScheduler() {
|
|
985
|
+
const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
|
|
986
|
+
process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
|
|
987
|
+
if (daemonConfig.embedOnStartup) setTimeout(() => {
|
|
988
|
+
runEmbed().catch((e) => {
|
|
989
|
+
process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
|
|
990
|
+
});
|
|
991
|
+
}, 6e4);
|
|
992
|
+
else process.stderr.write("[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\n");
|
|
993
|
+
if (daemonConfig.maintenanceHour !== void 0) {
|
|
994
|
+
process.stderr.write("[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; embed runs at the end of each anchored index cycle).\n");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const timer = setInterval(() => {
|
|
998
|
+
runEmbed().catch((e) => {
|
|
999
|
+
process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
|
|
1000
|
+
});
|
|
1001
|
+
}, intervalMs);
|
|
1002
|
+
if (timer.unref) timer.unref();
|
|
1003
|
+
setEmbedSchedulerTimer(timer);
|
|
1004
|
+
}
|
|
1005
|
+
const REGISTRY_SCAN_INTERVAL_MS = 1800 * 1e3;
|
|
1006
|
+
const REGISTRY_SCAN_STARTUP_DELAY_MS = 1e4;
|
|
1007
|
+
/**
|
|
1008
|
+
* Start the periodic registry scan scheduler.
|
|
1009
|
+
* First scan runs 10 seconds after startup (enough time for IPC to stabilise).
|
|
1010
|
+
* Subsequent scans run every 30 minutes.
|
|
1011
|
+
* Uses the work-queue so the scan runs on the worker thread without blocking IPC.
|
|
1012
|
+
*/
|
|
1013
|
+
function startRegistryScanScheduler() {
|
|
1014
|
+
process.stderr.write("[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\n");
|
|
1015
|
+
setTimeout(() => {
|
|
1016
|
+
import("./work-queue-worker-AEmfVE0F.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
1017
|
+
process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\n`);
|
|
1018
|
+
});
|
|
1019
|
+
}, REGISTRY_SCAN_STARTUP_DELAY_MS);
|
|
1020
|
+
const timer = setInterval(() => {
|
|
1021
|
+
import("./work-queue-worker-AEmfVE0F.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
1022
|
+
process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\n`);
|
|
1023
|
+
});
|
|
1024
|
+
}, REGISTRY_SCAN_INTERVAL_MS);
|
|
1025
|
+
if (timer.unref) timer.unref();
|
|
1026
|
+
}
|
|
1027
|
+
/** How often the daemon looks at the worker ledger for supervision events. */
|
|
1028
|
+
const SUPERVISION_TICK_MS = 3e4;
|
|
1029
|
+
/** First supervision pass waits for the first statuses to exist at all. */
|
|
1030
|
+
const SUPERVISION_STARTUP_DELAY_MS = 15e3;
|
|
1031
|
+
/**
|
|
1032
|
+
* Start the worker supervisor: one tick every 30 s over the workers ledger
|
|
1033
|
+
* (src/workers/supervision.ts) that pushes finished/failed/stalled events to
|
|
1034
|
+
* the owning session — the daemon-side replacement for orchestrator polling.
|
|
1035
|
+
* Daemon code only: no model call has any business being here, and none is
|
|
1036
|
+
* made. Disabled workers disable the supervisor with it.
|
|
1037
|
+
*/
|
|
1038
|
+
function startWorkerSupervisor() {
|
|
1039
|
+
let enabled = false;
|
|
1040
|
+
try {
|
|
1041
|
+
enabled = readWorkersSection().workers.enabled;
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
process.stderr.write(`[pai-daemon] Worker supervisor: not started (${e instanceof Error ? e.message : String(e)})\n`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (!enabled) {
|
|
1047
|
+
process.stderr.write("[pai-daemon] Worker supervisor: disabled (workers are off)\n");
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const tick = () => {
|
|
1051
|
+
runSupervisionTick(workersLogDir(readWorkersSection().workers), { stallMs: stallMinutesFromEnv() * 6e4 }).then((r) => {
|
|
1052
|
+
for (const ev of r.events) process.stderr.write(`[pai-daemon] Supervision: ${ev.text}\n`);
|
|
1053
|
+
}).catch((e) => {
|
|
1054
|
+
process.stderr.write(`[pai-daemon] Supervision tick error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
const first = setTimeout(tick, SUPERVISION_STARTUP_DELAY_MS);
|
|
1058
|
+
if (first.unref) first.unref();
|
|
1059
|
+
const timer = setInterval(tick, SUPERVISION_TICK_MS);
|
|
1060
|
+
if (timer.unref) timer.unref();
|
|
1061
|
+
process.stderr.write(`[pai-daemon] Worker supervisor: every ${Math.round(SUPERVISION_TICK_MS / 1e3)}s\n`);
|
|
1062
|
+
}
|
|
1063
|
+
/** First beat waits a while after boot; the daemon start is itself a warm request. */
|
|
1064
|
+
const KEEPALIVE_STARTUP_DELAY_MS = 6e4;
|
|
1065
|
+
/**
|
|
1066
|
+
* Start the cache keepalive: every workers.cacheKeepaliveSecs one trivial
|
|
1067
|
+
* single-turn worker (src/workers/keepalive.ts) re-arms the provider's
|
|
1068
|
+
* implicit prompt cache so real worker spawns start warm. Needs no storage
|
|
1069
|
+
* backend — it starts with the IPC server, not after the federation connect.
|
|
1070
|
+
* 0 (or workers off) keeps it off. A failed beat is logged and swallowed.
|
|
1071
|
+
*/
|
|
1072
|
+
function startCacheKeepalive(opts = {}) {
|
|
1073
|
+
let workers;
|
|
1074
|
+
try {
|
|
1075
|
+
workers = readWorkersSection(opts.configPath).workers;
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
process.stderr.write(`[pai-daemon] Cache keepalive: not started (${e instanceof Error ? e.message : String(e)})\n`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (!workers.enabled) {
|
|
1081
|
+
process.stderr.write("[pai-daemon] Cache keepalive: disabled (workers are off)\n");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const secs = keepaliveSecs(workers);
|
|
1085
|
+
if (!secs) {
|
|
1086
|
+
process.stderr.write("[pai-daemon] Cache keepalive: disabled (cacheKeepaliveSecs is 0)\n");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let notedNoPostgres = false;
|
|
1090
|
+
const sqliteNote = () => {
|
|
1091
|
+
const pool = storageBackend?.getPool?.();
|
|
1092
|
+
if (pool) return pool;
|
|
1093
|
+
if (!notedNoPostgres) {
|
|
1094
|
+
notedNoPostgres = true;
|
|
1095
|
+
process.stderr.write("[pai-daemon] Cache keepalive: observation rows need Postgres — ledger line only\n");
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
};
|
|
1099
|
+
const beat = async () => {
|
|
1100
|
+
const m = await (opts.beat ?? runKeepaliveBeat)();
|
|
1101
|
+
if (!m) return;
|
|
1102
|
+
const pool = sqliteNote();
|
|
1103
|
+
if (!pool) return;
|
|
1104
|
+
try {
|
|
1105
|
+
await storeObservationWithProject(registryDb, pool, {
|
|
1106
|
+
session_id: m.id,
|
|
1107
|
+
type: "change",
|
|
1108
|
+
title: `cache keepalive ${m.ok ? "ok" : "failed"} (cache_read=${m.cache_read_input_tokens}, api_ms=${m.duration_api_ms})`,
|
|
1109
|
+
tool_name: "cache-keepalive",
|
|
1110
|
+
narrative: `beat ${m.id}: input=${m.input_tokens} cache_read=${m.cache_read_input_tokens} cache_creation=${m.cache_creation_input_tokens} duration_api_ms=${m.duration_api_ms}`,
|
|
1111
|
+
tool_input_summary: void 0,
|
|
1112
|
+
files_read: [],
|
|
1113
|
+
files_modified: [],
|
|
1114
|
+
concepts: []
|
|
1115
|
+
});
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
process.stderr.write(`[pai-daemon] Cache keepalive observation error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const first = setTimeout(() => {
|
|
1121
|
+
beat().catch((e) => {
|
|
1122
|
+
process.stderr.write(`[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1123
|
+
});
|
|
1124
|
+
}, KEEPALIVE_STARTUP_DELAY_MS);
|
|
1125
|
+
if (first.unref) first.unref();
|
|
1126
|
+
const timer = setInterval(() => {
|
|
1127
|
+
beat().catch((e) => {
|
|
1128
|
+
process.stderr.write(`[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1129
|
+
});
|
|
1130
|
+
}, secs * 1e3);
|
|
1131
|
+
if (timer.unref) timer.unref();
|
|
1132
|
+
setCacheKeepaliveTimer(timer);
|
|
1133
|
+
process.stderr.write(`[pai-daemon] Cache keepalive: every ${secs}s\n`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
906
1136
|
//#endregion
|
|
907
1137
|
//#region src/daemon/daemon/dispatcher.ts
|
|
908
1138
|
/**
|
|
@@ -1174,33 +1404,22 @@ async function handleRequest(request, socket) {
|
|
|
1174
1404
|
}
|
|
1175
1405
|
try {
|
|
1176
1406
|
const p = params;
|
|
1177
|
-
let project_id = null;
|
|
1178
|
-
let project_slug = null;
|
|
1179
|
-
if (p.cwd) {
|
|
1180
|
-
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);
|
|
1181
|
-
if (row) {
|
|
1182
|
-
project_id = row.id;
|
|
1183
|
-
project_slug = row.slug;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
await ensureObservationTables(pool);
|
|
1187
1407
|
sendResponse(socket, {
|
|
1188
1408
|
id,
|
|
1189
1409
|
ok: true,
|
|
1190
1410
|
result: {
|
|
1191
1411
|
ok: true,
|
|
1192
|
-
id: await
|
|
1412
|
+
id: await storeObservationWithProject(registryDb, pool, {
|
|
1193
1413
|
session_id: p.session_id,
|
|
1194
|
-
project_id,
|
|
1195
|
-
project_slug,
|
|
1196
1414
|
type: p.type,
|
|
1197
1415
|
title: p.title,
|
|
1198
1416
|
narrative: p.narrative ?? null,
|
|
1199
1417
|
tool_name: p.tool_name,
|
|
1200
|
-
tool_input_summary: p.tool_input_summary ??
|
|
1418
|
+
tool_input_summary: p.tool_input_summary ?? void 0,
|
|
1201
1419
|
files_read: p.files_read ?? [],
|
|
1202
1420
|
files_modified: p.files_modified ?? [],
|
|
1203
|
-
concepts: p.concepts ?? []
|
|
1421
|
+
concepts: p.concepts ?? [],
|
|
1422
|
+
cwd: p.cwd
|
|
1204
1423
|
})
|
|
1205
1424
|
}
|
|
1206
1425
|
});
|
|
@@ -1641,7 +1860,7 @@ async function serve(config) {
|
|
|
1641
1860
|
process.stderr.write("[pai-daemon] Starting daemon...\n");
|
|
1642
1861
|
process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
|
|
1643
1862
|
process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
|
|
1644
|
-
const { notificationConfig } = await import("./state-
|
|
1863
|
+
const { notificationConfig } = await import("./state-CY6I-Hnc.mjs");
|
|
1645
1864
|
process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
|
|
1646
1865
|
try {
|
|
1647
1866
|
setPriority(process.pid, 10);
|
|
@@ -1658,6 +1877,7 @@ async function serve(config) {
|
|
|
1658
1877
|
const server = await startIpcServer(config.socketPath);
|
|
1659
1878
|
startRegistryScanScheduler();
|
|
1660
1879
|
startWorkerSupervisor();
|
|
1880
|
+
startCacheKeepalive();
|
|
1661
1881
|
createStorageBackend(config, { waitForPostgres: true }).then((backend) => {
|
|
1662
1882
|
setStorageBackend(backend);
|
|
1663
1883
|
process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
|
|
@@ -1676,6 +1896,7 @@ async function serve(config) {
|
|
|
1676
1896
|
setShutdownRequested(true);
|
|
1677
1897
|
if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
|
|
1678
1898
|
if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
|
|
1899
|
+
if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);
|
|
1679
1900
|
stopWorker();
|
|
1680
1901
|
server.close();
|
|
1681
1902
|
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
@@ -1706,4 +1927,4 @@ async function serve(config) {
|
|
|
1706
1927
|
|
|
1707
1928
|
//#endregion
|
|
1708
1929
|
export { serve as t };
|
|
1709
|
-
//# sourceMappingURL=daemon-
|
|
1930
|
+
//# sourceMappingURL=daemon-Dq8A1uDF.mjs.map
|