@tekmidian/pai 0.42.1 → 0.42.3
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-Bi_F6lXf.mjs → config-_lQI_W08.mjs} +87 -3
- package/dist/config-_lQI_W08.mjs.map +1 -0
- package/dist/{context-handover-cache--bm6Siim.mjs → context-handover-cache-pNFdBNrU.mjs} +3 -3
- package/dist/{context-handover-cache--bm6Siim.mjs.map → context-handover-cache-pNFdBNrU.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/{daemon-BOAPyL_b.mjs → daemon-Awt_3UJp.mjs} +7 -7
- package/dist/{daemon-DvXih78C.mjs → daemon-Dq8A1uDF.mjs} +587 -368
- package/dist/daemon-Dq8A1uDF.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +5 -5
- package/dist/{fallback-8bzmg6No.mjs → fallback-vQPI1qxt.mjs} +14 -12
- 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 +64 -1
- 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-CqppP_Bw.mjs → main-resolver-BAfOMppW.mjs} +2 -2
- package/dist/{main-resolver-Dip2VLB5.mjs → main-resolver-DnkxPYkr.mjs} +111 -16
- package/dist/main-resolver-DnkxPYkr.mjs.map +1 -0
- package/dist/{planner-ogy-Zy4O.mjs → planner-BfEGcEs1.mjs} +5 -5
- package/dist/{planner-ogy-Zy4O.mjs.map → planner-BfEGcEs1.mjs.map} +1 -1
- package/dist/{program-B7N5OIJR.mjs → program-BDmeZP-v.mjs} +17 -11
- package/dist/{program-B7N5OIJR.mjs.map → program-BDmeZP-v.mjs.map} +1 -1
- package/dist/{chain-DsvSHnO9.mjs → run-CGXbhf2k.mjs} +359 -163
- package/dist/run-CGXbhf2k.mjs.map +1 -0
- package/dist/{server-DvN-IFIV.mjs → server-D4vAieF4.mjs} +25 -6
- 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-19r0cpH4.mjs → work-queue-worker-AEmfVE0F.mjs} +5 -5
- package/dist/{work-queue-worker-DXh_FnT5.mjs → work-queue-worker-DIT-VLSh.mjs} +3 -3
- package/dist/{work-queue-worker-DXh_FnT5.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/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-DsvSHnO9.mjs.map +0 -1
- package/dist/config-Bi_F6lXf.mjs.map +0 -1
- package/dist/daemon-DvXih78C.mjs.map +0 -1
- package/dist/fallback-8bzmg6No.mjs.map +0 -1
- package/dist/main-resolver-Dip2VLB5.mjs.map +0 -1
- package/dist/scope-CfYnjy5R.mjs +0 -360
- package/dist/scope-CfYnjy5R.mjs.map +0 -1
- package/dist/server-DvN-IFIV.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
|
/**
|
|
@@ -379,368 +514,132 @@ async function runSupervisionTick(logDir, opts = {}) {
|
|
|
379
514
|
}
|
|
380
515
|
|
|
381
516
|
//#endregion
|
|
382
|
-
//#region src/
|
|
517
|
+
//#region src/observations/store.ts
|
|
383
518
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
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.
|
|
387
527
|
*/
|
|
388
|
-
|
|
389
|
-
const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
|
|
528
|
+
let _tablesEnsured = false;
|
|
390
529
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
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/).
|
|
393
532
|
*/
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
`;
|
|
429
574
|
/**
|
|
430
|
-
* Run
|
|
431
|
-
*
|
|
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).
|
|
432
578
|
*/
|
|
433
|
-
async function
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (indexInProgress || embedInProgress) {
|
|
440
|
-
process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
const { lastVaultIndexTime } = await import("./state-RXepvbnO.mjs");
|
|
444
|
-
if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
|
|
445
|
-
let vaultProjectId = daemonConfig.vaultProjectId;
|
|
446
|
-
if (!vaultProjectId) {
|
|
447
|
-
const row = registryDb.prepare("SELECT id FROM projects WHERE root_path = ?").get(daemonConfig.vaultPath);
|
|
448
|
-
vaultProjectId = row?.id ?? 999;
|
|
449
|
-
if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
|
|
450
|
-
}
|
|
451
|
-
setVaultIndexInProgress(true);
|
|
452
|
-
const t0 = Date.now();
|
|
453
|
-
process.stderr.write("[pai-daemon] Starting vault index run...\n");
|
|
454
|
-
try {
|
|
455
|
-
const { indexVault } = await import("./vault-indexer-mata9S7b.mjs");
|
|
456
|
-
const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
|
|
457
|
-
const elapsed = Date.now() - t0;
|
|
458
|
-
setLastVaultIndexTime(Date.now());
|
|
459
|
-
process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
462
|
-
process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
|
|
463
|
-
} finally {
|
|
464
|
-
setVaultIndexInProgress(false);
|
|
465
|
-
}
|
|
579
|
+
async function ensureObservationTables(pool) {
|
|
580
|
+
if (_tablesEnsured) return;
|
|
581
|
+
await pool.query(SCHEMA_SQL);
|
|
582
|
+
_tablesEnsured = true;
|
|
466
583
|
}
|
|
467
584
|
/**
|
|
468
|
-
*
|
|
585
|
+
* Compute a 16-character hex content hash for deduplication.
|
|
586
|
+
* Hash = SHA256(session_id + tool_name + title).slice(0, 16)
|
|
469
587
|
*/
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
|
|
473
|
-
const cycle = (label, withEmbed) => runIndex().then(() => runVaultIndex()).then(() => withEmbed ? runEmbed() : void 0).catch((e) => {
|
|
474
|
-
process.stderr.write(`[pai-daemon] ${label} index error: ${e}\n`);
|
|
475
|
-
});
|
|
476
|
-
if (daemonConfig.maintenanceHour === void 0) setTimeout(() => void cycle("Startup", daemonConfig.embedOnStartup), 2e3);
|
|
477
|
-
else process.stderr.write("[pai-daemon] Startup index pass skipped (maintenanceHour is set).\n");
|
|
478
|
-
const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);
|
|
479
|
-
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`);
|
|
480
|
-
let timer;
|
|
481
|
-
const startTimer = () => {
|
|
482
|
-
timer = setInterval(() => void cycle("Scheduled", true), intervalMs);
|
|
483
|
-
if (timer.unref) timer.unref();
|
|
484
|
-
setIndexSchedulerTimer(timer);
|
|
485
|
-
};
|
|
486
|
-
const first = setTimeout(() => {
|
|
487
|
-
cycle("Scheduled", true);
|
|
488
|
-
startTimer();
|
|
489
|
-
}, firstDelayMs);
|
|
490
|
-
if (first.unref) first.unref();
|
|
588
|
+
function computeContentHash(sessionId, toolName, title) {
|
|
589
|
+
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
491
590
|
}
|
|
492
591
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
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.
|
|
495
594
|
*/
|
|
496
|
-
function
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
503
622
|
}
|
|
504
623
|
/**
|
|
505
|
-
*
|
|
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.
|
|
506
627
|
*/
|
|
507
|
-
async function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
637
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const projectNames = /* @__PURE__ */ new Map();
|
|
522
|
-
try {
|
|
523
|
-
const rows = registryDb.prepare("SELECT id, slug FROM projects WHERE status = 'active'").all();
|
|
524
|
-
for (const r of rows) projectNames.set(r.id, r.slug);
|
|
525
|
-
} catch {}
|
|
526
|
-
const { embedChunksWithBackend } = await import("./indexer-backend-BPTzKFZU.mjs");
|
|
527
|
-
const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames, { maxMillis: 24e4 });
|
|
528
|
-
let vaultEmbedCount = 0;
|
|
529
|
-
if (daemonConfig.vaultPath) try {
|
|
530
|
-
const { SQLiteBackend } = await import("./sqlite-Cq3gJcq9.mjs");
|
|
531
|
-
const { openFederation } = await import("./db-O-cyAPfS.mjs");
|
|
532
|
-
const federationDb = openFederation();
|
|
533
|
-
const vaultSqliteBackend = new SQLiteBackend(federationDb);
|
|
534
|
-
const vaultProjectNames = new Map(projectNames);
|
|
535
|
-
if (!vaultProjectNames.has(999)) vaultProjectNames.set(999, "obsidian-vault");
|
|
536
|
-
vaultEmbedCount = await embedChunksWithBackend(vaultSqliteBackend, () => shutdownRequested, vaultProjectNames, { maxMillis: 45e3 });
|
|
537
|
-
try {
|
|
538
|
-
federationDb.close();
|
|
539
|
-
} catch {}
|
|
540
|
-
if (vaultEmbedCount > 0) process.stderr.write(`[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\n`);
|
|
541
|
-
} catch (ve) {
|
|
542
|
-
const vmsg = ve instanceof Error ? ve.message : String(ve);
|
|
543
|
-
process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\n`);
|
|
544
|
-
}
|
|
545
|
-
const elapsed = Date.now() - t0;
|
|
546
|
-
setLastEmbedTime(Date.now());
|
|
547
|
-
process.stderr.write(`[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\n`);
|
|
548
|
-
} catch (e) {
|
|
549
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
550
|
-
process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
|
|
551
|
-
} finally {
|
|
552
|
-
setEmbedInProgress(false);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Start the periodic embed scheduler. Initial run is 60 seconds after startup.
|
|
557
|
-
*/
|
|
558
|
-
function startEmbedScheduler() {
|
|
559
|
-
const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
|
|
560
|
-
process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
|
|
561
|
-
if (daemonConfig.embedOnStartup) setTimeout(() => {
|
|
562
|
-
runEmbed().catch((e) => {
|
|
563
|
-
process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
|
|
564
|
-
});
|
|
565
|
-
}, 6e4);
|
|
566
|
-
else process.stderr.write("[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\n");
|
|
567
|
-
if (daemonConfig.maintenanceHour !== void 0) {
|
|
568
|
-
process.stderr.write("[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; embed runs at the end of each anchored index cycle).\n");
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
const timer = setInterval(() => {
|
|
572
|
-
runEmbed().catch((e) => {
|
|
573
|
-
process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
|
|
574
|
-
});
|
|
575
|
-
}, intervalMs);
|
|
576
|
-
if (timer.unref) timer.unref();
|
|
577
|
-
setEmbedSchedulerTimer(timer);
|
|
578
|
-
}
|
|
579
|
-
const REGISTRY_SCAN_INTERVAL_MS = 1800 * 1e3;
|
|
580
|
-
const REGISTRY_SCAN_STARTUP_DELAY_MS = 1e4;
|
|
581
|
-
/**
|
|
582
|
-
* Start the periodic registry scan scheduler.
|
|
583
|
-
* First scan runs 10 seconds after startup (enough time for IPC to stabilise).
|
|
584
|
-
* Subsequent scans run every 30 minutes.
|
|
585
|
-
* Uses the work-queue so the scan runs on the worker thread without blocking IPC.
|
|
586
|
-
*/
|
|
587
|
-
function startRegistryScanScheduler() {
|
|
588
|
-
process.stderr.write("[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\n");
|
|
589
|
-
setTimeout(() => {
|
|
590
|
-
import("./work-queue-worker-19r0cpH4.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
591
|
-
process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\n`);
|
|
592
|
-
});
|
|
593
|
-
}, REGISTRY_SCAN_STARTUP_DELAY_MS);
|
|
594
|
-
const timer = setInterval(() => {
|
|
595
|
-
import("./work-queue-worker-19r0cpH4.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
|
|
596
|
-
process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\n`);
|
|
597
|
-
});
|
|
598
|
-
}, REGISTRY_SCAN_INTERVAL_MS);
|
|
599
|
-
if (timer.unref) timer.unref();
|
|
600
|
-
}
|
|
601
|
-
/** How often the daemon looks at the worker ledger for supervision events. */
|
|
602
|
-
const SUPERVISION_TICK_MS = 3e4;
|
|
603
|
-
/** First supervision pass waits for the first statuses to exist at all. */
|
|
604
|
-
const SUPERVISION_STARTUP_DELAY_MS = 15e3;
|
|
605
|
-
/**
|
|
606
|
-
* Start the worker supervisor: one tick every 30 s over the workers ledger
|
|
607
|
-
* (src/workers/supervision.ts) that pushes finished/failed/stalled events to
|
|
608
|
-
* the owning session — the daemon-side replacement for orchestrator polling.
|
|
609
|
-
* Daemon code only: no model call has any business being here, and none is
|
|
610
|
-
* made. Disabled workers disable the supervisor with it.
|
|
611
|
-
*/
|
|
612
|
-
function startWorkerSupervisor() {
|
|
613
|
-
let enabled = false;
|
|
614
|
-
try {
|
|
615
|
-
enabled = readWorkersSection().workers.enabled;
|
|
616
|
-
} catch (e) {
|
|
617
|
-
process.stderr.write(`[pai-daemon] Worker supervisor: not started (${e instanceof Error ? e.message : String(e)})\n`);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
if (!enabled) {
|
|
621
|
-
process.stderr.write("[pai-daemon] Worker supervisor: disabled (workers are off)\n");
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
const tick = () => {
|
|
625
|
-
runSupervisionTick(workersLogDir(readWorkersSection().workers), { stallMs: stallMinutesFromEnv() * 6e4 }).then((r) => {
|
|
626
|
-
for (const ev of r.events) process.stderr.write(`[pai-daemon] Supervision: ${ev.text}\n`);
|
|
627
|
-
}).catch((e) => {
|
|
628
|
-
process.stderr.write(`[pai-daemon] Supervision tick error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
629
|
-
});
|
|
630
|
-
};
|
|
631
|
-
const first = setTimeout(tick, SUPERVISION_STARTUP_DELAY_MS);
|
|
632
|
-
if (first.unref) first.unref();
|
|
633
|
-
const timer = setInterval(tick, SUPERVISION_TICK_MS);
|
|
634
|
-
if (timer.unref) timer.unref();
|
|
635
|
-
process.stderr.write(`[pai-daemon] Worker supervisor: every ${Math.round(SUPERVISION_TICK_MS / 1e3)}s\n`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/observations/store.ts
|
|
640
|
-
/**
|
|
641
|
-
* store.ts — PostgreSQL persistence for PAI observations.
|
|
642
|
-
*
|
|
643
|
-
* All functions accept a pg.Pool and are safe to call concurrently.
|
|
644
|
-
* Schema is initialized lazily via ensureObservationTables().
|
|
645
|
-
*
|
|
646
|
-
* Content-hash deduplication: observations with the same hash
|
|
647
|
-
* created within a 30-second window are silently dropped to prevent
|
|
648
|
-
* duplicate entries from rapid repeated tool calls.
|
|
649
|
-
*/
|
|
650
|
-
let _tablesEnsured = false;
|
|
651
|
-
/**
|
|
652
|
-
* Inlined schema DDL — avoids runtime file reads that break in bundled code
|
|
653
|
-
* (the bundler puts this in a shared chunk whose __dirname differs from src/).
|
|
654
|
-
*/
|
|
655
|
-
const SCHEMA_SQL = `
|
|
656
|
-
CREATE TABLE IF NOT EXISTS pai_observations (
|
|
657
|
-
id SERIAL PRIMARY KEY,
|
|
658
|
-
session_id TEXT NOT NULL,
|
|
659
|
-
project_id INTEGER,
|
|
660
|
-
project_slug TEXT,
|
|
661
|
-
type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
|
662
|
-
title TEXT NOT NULL,
|
|
663
|
-
narrative TEXT,
|
|
664
|
-
tool_name TEXT,
|
|
665
|
-
tool_input_summary TEXT,
|
|
666
|
-
files_read JSONB DEFAULT '[]'::jsonb,
|
|
667
|
-
files_modified JSONB DEFAULT '[]'::jsonb,
|
|
668
|
-
concepts JSONB DEFAULT '[]'::jsonb,
|
|
669
|
-
content_hash TEXT,
|
|
670
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
CREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);
|
|
674
|
-
CREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);
|
|
675
|
-
CREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);
|
|
676
|
-
CREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);
|
|
677
|
-
CREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);
|
|
678
|
-
|
|
679
|
-
CREATE TABLE IF NOT EXISTS pai_session_summaries (
|
|
680
|
-
id SERIAL PRIMARY KEY,
|
|
681
|
-
session_id TEXT NOT NULL UNIQUE,
|
|
682
|
-
project_id INTEGER,
|
|
683
|
-
project_slug TEXT,
|
|
684
|
-
request TEXT,
|
|
685
|
-
investigated TEXT,
|
|
686
|
-
learned TEXT,
|
|
687
|
-
completed TEXT,
|
|
688
|
-
next_steps TEXT,
|
|
689
|
-
observation_count INTEGER DEFAULT 0,
|
|
690
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
CREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);
|
|
694
|
-
CREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);
|
|
695
|
-
`;
|
|
696
|
-
/**
|
|
697
|
-
* Run schema DDL idempotently against the given pool.
|
|
698
|
-
* Uses a module-level flag so subsequent calls are no-ops within the same
|
|
699
|
-
* process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).
|
|
700
|
-
*/
|
|
701
|
-
async function ensureObservationTables(pool) {
|
|
702
|
-
if (_tablesEnsured) return;
|
|
703
|
-
await pool.query(SCHEMA_SQL);
|
|
704
|
-
_tablesEnsured = true;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Compute a 16-character hex content hash for deduplication.
|
|
708
|
-
* Hash = SHA256(session_id + tool_name + title).slice(0, 16)
|
|
709
|
-
*/
|
|
710
|
-
function computeContentHash(sessionId, toolName, title) {
|
|
711
|
-
return sha256(sessionId + "\0" + toolName + "\0" + title).slice(0, 16);
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Insert an observation, skipping duplicates within a 30-second window.
|
|
715
|
-
* Returns the inserted row's id, or null if the insert was suppressed.
|
|
716
|
-
*/
|
|
717
|
-
async function storeObservation(pool, obs) {
|
|
718
|
-
await ensureObservationTables(pool);
|
|
719
|
-
const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);
|
|
720
|
-
const dupCheck = await pool.query(`SELECT id FROM pai_observations
|
|
721
|
-
WHERE content_hash = $1
|
|
722
|
-
AND session_id = $2
|
|
723
|
-
AND created_at >= NOW() - INTERVAL '30 seconds'
|
|
724
|
-
LIMIT 1`, [hash, obs.session_id]);
|
|
725
|
-
if (dupCheck.rowCount && dupCheck.rowCount > 0) return null;
|
|
726
|
-
return (await pool.query(`INSERT INTO pai_observations
|
|
727
|
-
(session_id, project_id, project_slug, type, title, narrative,
|
|
728
|
-
tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)
|
|
729
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)
|
|
730
|
-
RETURNING id`, [
|
|
731
|
-
obs.session_id,
|
|
732
|
-
obs.project_id ?? null,
|
|
733
|
-
obs.project_slug ?? null,
|
|
734
|
-
obs.type,
|
|
735
|
-
obs.title,
|
|
736
|
-
obs.narrative ?? null,
|
|
737
|
-
obs.tool_name,
|
|
738
|
-
obs.tool_input_summary ?? null,
|
|
739
|
-
JSON.stringify(obs.files_read),
|
|
740
|
-
JSON.stringify(obs.files_modified),
|
|
741
|
-
JSON.stringify(obs.concepts),
|
|
742
|
-
hash
|
|
743
|
-
])).rows[0]?.id ?? null;
|
|
638
|
+
return storeObservation(pool, {
|
|
639
|
+
...obs,
|
|
640
|
+
project_id,
|
|
641
|
+
project_slug
|
|
642
|
+
});
|
|
744
643
|
}
|
|
745
644
|
/**
|
|
746
645
|
* Filtered query for observations with optional projectId, sessionId, type,
|
|
@@ -905,6 +804,335 @@ async function querySkillTelemetry(pool, opts = {}) {
|
|
|
905
804
|
LIMIT $${idx}`, params)).rows;
|
|
906
805
|
}
|
|
907
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
|
+
|
|
908
1136
|
//#endregion
|
|
909
1137
|
//#region src/daemon/daemon/dispatcher.ts
|
|
910
1138
|
/**
|
|
@@ -1176,33 +1404,22 @@ async function handleRequest(request, socket) {
|
|
|
1176
1404
|
}
|
|
1177
1405
|
try {
|
|
1178
1406
|
const p = params;
|
|
1179
|
-
let project_id = null;
|
|
1180
|
-
let project_slug = null;
|
|
1181
|
-
if (p.cwd) {
|
|
1182
|
-
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);
|
|
1183
|
-
if (row) {
|
|
1184
|
-
project_id = row.id;
|
|
1185
|
-
project_slug = row.slug;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
await ensureObservationTables(pool);
|
|
1189
1407
|
sendResponse(socket, {
|
|
1190
1408
|
id,
|
|
1191
1409
|
ok: true,
|
|
1192
1410
|
result: {
|
|
1193
1411
|
ok: true,
|
|
1194
|
-
id: await
|
|
1412
|
+
id: await storeObservationWithProject(registryDb, pool, {
|
|
1195
1413
|
session_id: p.session_id,
|
|
1196
|
-
project_id,
|
|
1197
|
-
project_slug,
|
|
1198
1414
|
type: p.type,
|
|
1199
1415
|
title: p.title,
|
|
1200
1416
|
narrative: p.narrative ?? null,
|
|
1201
1417
|
tool_name: p.tool_name,
|
|
1202
|
-
tool_input_summary: p.tool_input_summary ??
|
|
1418
|
+
tool_input_summary: p.tool_input_summary ?? void 0,
|
|
1203
1419
|
files_read: p.files_read ?? [],
|
|
1204
1420
|
files_modified: p.files_modified ?? [],
|
|
1205
|
-
concepts: p.concepts ?? []
|
|
1421
|
+
concepts: p.concepts ?? [],
|
|
1422
|
+
cwd: p.cwd
|
|
1206
1423
|
})
|
|
1207
1424
|
}
|
|
1208
1425
|
});
|
|
@@ -1643,7 +1860,7 @@ async function serve(config) {
|
|
|
1643
1860
|
process.stderr.write("[pai-daemon] Starting daemon...\n");
|
|
1644
1861
|
process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
|
|
1645
1862
|
process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
|
|
1646
|
-
const { notificationConfig } = await import("./state-
|
|
1863
|
+
const { notificationConfig } = await import("./state-CY6I-Hnc.mjs");
|
|
1647
1864
|
process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
|
|
1648
1865
|
try {
|
|
1649
1866
|
setPriority(process.pid, 10);
|
|
@@ -1660,6 +1877,7 @@ async function serve(config) {
|
|
|
1660
1877
|
const server = await startIpcServer(config.socketPath);
|
|
1661
1878
|
startRegistryScanScheduler();
|
|
1662
1879
|
startWorkerSupervisor();
|
|
1880
|
+
startCacheKeepalive();
|
|
1663
1881
|
createStorageBackend(config, { waitForPostgres: true }).then((backend) => {
|
|
1664
1882
|
setStorageBackend(backend);
|
|
1665
1883
|
process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
|
|
@@ -1678,6 +1896,7 @@ async function serve(config) {
|
|
|
1678
1896
|
setShutdownRequested(true);
|
|
1679
1897
|
if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
|
|
1680
1898
|
if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
|
|
1899
|
+
if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);
|
|
1681
1900
|
stopWorker();
|
|
1682
1901
|
server.close();
|
|
1683
1902
|
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
@@ -1708,4 +1927,4 @@ async function serve(config) {
|
|
|
1708
1927
|
|
|
1709
1928
|
//#endregion
|
|
1710
1929
|
export { serve as t };
|
|
1711
|
-
//# sourceMappingURL=daemon-
|
|
1930
|
+
//# sourceMappingURL=daemon-Dq8A1uDF.mjs.map
|