@tekmidian/pai 0.66.1 → 0.66.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.
Files changed (158) hide show
  1. package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
  2. package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
  3. package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
  4. package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
  5. package/dist/auto-route-BnizyALK.mjs.map +1 -0
  6. package/dist/auto-route-D6fW1Q8z.mjs +3 -0
  7. package/dist/{chain-CZfKIEq9.mjs → chain-D5876UQX.mjs} +3 -4
  8. package/dist/{chain-CZfKIEq9.mjs.map → chain-D5876UQX.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +18 -23
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +17 -22
  12. package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
  13. package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
  14. package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
  15. package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
  16. package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
  17. package/dist/daemon/index.mjs +17 -19
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/daemon-BUgNvMUh.mjs +5029 -0
  20. package/dist/daemon-BUgNvMUh.mjs.map +1 -0
  21. package/dist/daemon-DL0eglMJ.mjs +18 -0
  22. package/dist/daemon-mcp/index.mjs +12 -9
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
  25. package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
  26. package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
  27. package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
  28. package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
  29. package/dist/factory-DrBg24UT.mjs +8 -0
  30. package/dist/factory-Ypf8r0fK.mjs +2892 -0
  31. package/dist/factory-Ypf8r0fK.mjs.map +1 -0
  32. package/dist/{fallback-CIiq2Raw.mjs → fallback-D51R4shY.mjs} +23 -11
  33. package/dist/fallback-D51R4shY.mjs.map +1 -0
  34. package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
  35. package/dist/helpers-CZsi_49C.mjs.map +1 -0
  36. package/dist/hooks/worker-status-line.mjs.map +2 -2
  37. package/dist/index.mjs +6 -7
  38. package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
  39. package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
  40. package/dist/main-resolver-BYgP86C1.mjs +7 -0
  41. package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
  42. package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
  43. package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
  44. package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
  45. package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
  46. package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
  47. package/dist/{planner-Dm5Acyo1.mjs → planner-D5yt0EfA.mjs} +10 -7
  48. package/dist/{planner-Dm5Acyo1.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
  49. package/dist/postgres-BmLr0MUm.mjs +5 -0
  50. package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
  51. package/dist/postgres-D3xc2RB4.mjs.map +1 -0
  52. package/dist/{program-JJ0TwSHI.mjs → program-DIXRtAFy.mjs} +77 -70
  53. package/dist/program-DIXRtAFy.mjs.map +1 -0
  54. package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
  55. package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
  56. package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
  57. package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
  58. package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
  59. package/dist/router-BK-hFeQ6.mjs +3 -0
  60. package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
  61. package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
  62. package/dist/{run-BCpbxqA5.mjs → run-CLYTOm0x.mjs} +599 -22
  63. package/dist/run-CLYTOm0x.mjs.map +1 -0
  64. package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
  65. package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
  66. package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
  67. package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
  68. package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
  69. package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
  70. package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
  71. package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
  72. package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
  73. package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
  74. package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
  75. package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
  76. package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
  77. package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
  78. package/dist/zettelkasten-BwV1pluL.mjs +5 -0
  79. package/docs/commands/worker.md +2 -0
  80. package/package.json +1 -1
  81. package/dist/async-C2Bm_Lal.mjs +0 -300
  82. package/dist/async-C2Bm_Lal.mjs.map +0 -1
  83. package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
  84. package/dist/chunker-BH4i-F2b.mjs.map +0 -1
  85. package/dist/clusters-BCtD3fbe.mjs +0 -169
  86. package/dist/clusters-BCtD3fbe.mjs.map +0 -1
  87. package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
  88. package/dist/daemon-DXlaVCkw.mjs +0 -1741
  89. package/dist/daemon-DXlaVCkw.mjs.map +0 -1
  90. package/dist/daemon-DyORmxZu.mjs +0 -19
  91. package/dist/detector-BEPFsINR.mjs +0 -3
  92. package/dist/detector-BRTtAWrM.mjs +0 -65
  93. package/dist/detector-BRTtAWrM.mjs.map +0 -1
  94. package/dist/factory-CNpPQQ-5.mjs +0 -249
  95. package/dist/factory-CNpPQQ-5.mjs.map +0 -1
  96. package/dist/factory-DY7x73mE.mjs +0 -3
  97. package/dist/fallback-CIiq2Raw.mjs.map +0 -1
  98. package/dist/federation-db-BTyoufBh.mjs +0 -139
  99. package/dist/federation-db-BTyoufBh.mjs.map +0 -1
  100. package/dist/federation-db-HfIFc7FG.mjs +0 -3
  101. package/dist/helpers-BRCJg0G3.mjs +0 -223
  102. package/dist/helpers-BRCJg0G3.mjs.map +0 -1
  103. package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
  104. package/dist/indexer-backend-isSLg6yE.mjs +0 -1
  105. package/dist/kg-entity-DCOcsVFD.mjs +0 -29
  106. package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
  107. package/dist/latent-ideas-wwAXquGe.mjs +0 -191
  108. package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
  109. package/dist/link-boost-CnI7UVMJ.mjs +0 -34
  110. package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
  111. package/dist/main-resolver-BKz_OuEh.mjs +0 -7
  112. package/dist/merge-2gqRPFu2.mjs +0 -3
  113. package/dist/merge-DgU9OgZy.mjs +0 -6
  114. package/dist/merge-DgU9OgZy.mjs.map +0 -1
  115. package/dist/module-paths-DdRzbkUI.mjs +0 -44
  116. package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
  117. package/dist/neighborhood-2FsmzoxG.mjs +0 -114
  118. package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
  119. package/dist/note-context-De63k8na.mjs +0 -106
  120. package/dist/note-context-De63k8na.mjs.map +0 -1
  121. package/dist/postgres-Ceqsa64C.mjs.map +0 -1
  122. package/dist/program-JJ0TwSHI.mjs.map +0 -1
  123. package/dist/query-feedback-B_iigYj-.mjs +0 -3
  124. package/dist/registry-db-C7voqML9.mjs +0 -213
  125. package/dist/registry-db-C7voqML9.mjs.map +0 -1
  126. package/dist/registry-db-JHPhA8vF.mjs +0 -3
  127. package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
  128. package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
  129. package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
  130. package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
  131. package/dist/router-CXUGsv85.mjs +0 -3
  132. package/dist/run-BCpbxqA5.mjs.map +0 -1
  133. package/dist/search-Bf3Kub3F.mjs +0 -3
  134. package/dist/server-DgmAHyFK.mjs +0 -403
  135. package/dist/server-DgmAHyFK.mjs.map +0 -1
  136. package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
  137. package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
  138. package/dist/sqlite-CqsTy6xo.mjs +0 -936
  139. package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
  140. package/dist/state-DW8zdweW.mjs +0 -3
  141. package/dist/state-HyjqTihC.mjs +0 -76
  142. package/dist/state-HyjqTihC.mjs.map +0 -1
  143. package/dist/themes-CTaOj3e1.mjs +0 -148
  144. package/dist/themes-CTaOj3e1.mjs.map +0 -1
  145. package/dist/tools-BcOKPBtg.mjs +0 -1489
  146. package/dist/tools-BcOKPBtg.mjs.map +0 -1
  147. package/dist/tools-C-_l0o8Z.mjs +0 -6
  148. package/dist/trace-CtV0RO6n.mjs +0 -137
  149. package/dist/trace-CtV0RO6n.mjs.map +0 -1
  150. package/dist/utils-BrX9FLeK.mjs +0 -3
  151. package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
  152. package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
  153. package/dist/wakeup-D8n33hYV.mjs +0 -335
  154. package/dist/wakeup-D8n33hYV.mjs.map +0 -1
  155. package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
  156. package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
  157. package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
  158. package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
@@ -1,1741 +0,0 @@
1
- import { n as createRegistryBackend, o as getBackendOutage, r as createStorageBackend } from "./factory-CNpPQQ-5.mjs";
2
- import { a as enqueue, c as loadQueue, o as getStats } from "./context-handover-cache-pkHzmL3c.mjs";
3
- import { S as DEFAULT_NOTIFICATION_CONFIG, d as readMainConfigRaw, f as writeMainConfigRaw } from "./config-BbLFD7Uf.mjs";
4
- import { D as readWorkersSection } from "./run-env-DRcp7A8K.mjs";
5
- import { G as loadStatuses, H as isChatPane, P as appendLedger, R as alive, W as loadStatus, n as runWorker, q as nowStamp, x as itermForClaudeSession } from "./run-BCpbxqA5.mjs";
6
- import { h as ledgerPath, m as eventsPath, v as workersLogDir } from "./server-DgmAHyFK.mjs";
7
- import { A as vaultIndexInProgress, C as setShutdownRequested, D as shutdownRequested, E as setVaultIndexInProgress, O as startTime, S as setRegistryBackend, T as setStorageBackend, _ as setIndexSchedulerTimer, a as indexInProgress, b as setLastVaultIndexTime, c as lastIndexTime, d as registryBackend, 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-HyjqTihC.mjs";
8
- import { n as runSessionKeepaliveTick } from "./session-keepalive-DxjTHGcK.mjs";
9
- import { t as configureEmbeddingModel } from "./embeddings-Bx3q0QOY.mjs";
10
- import { t as routeNotification } from "./router-a5G7q8_3.mjs";
11
- import { C as toolProjectInfo, D as toolMemoryGet, O as toolMemoryOutline, S as toolProjectHealth, T as toolProjectTodo, a as toolMemoryWakeup, b as toolSessionRoute, i as toolMemoryTaxonomy, k as toolMemorySearch, n as toolMemoryFeedback, t as toolMemoryKgSearch, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-BcOKPBtg.mjs";
12
- import { t as detectTopicShift } from "./detector-BRTtAWrM.mjs";
13
- import { i as stopWorker, n as notifyNewWork, r as startWorker } from "./work-queue-worker-4tLa5gpQ.mjs";
14
- import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
15
- import { setPriority } from "node:os";
16
- import { dirname, join } from "node:path";
17
- import { execFileSync } from "node:child_process";
18
- import { connect, createServer } from "node:net";
19
-
20
- //#region src/notifications/config.ts
21
- /**
22
- * config.ts — Notification config persistence helpers
23
- *
24
- * Reads and writes the `notifications` section of CONFIG_FILE (see
25
- * src/daemon/config.ts's paiConfigFilePath).
26
- * Deep-merges with defaults so partial configs work fine.
27
- *
28
- * This module is intentionally separate from the daemon's config loader
29
- * so it can be used standalone (e.g. from CLI commands).
30
- */
31
- function deepMerge(target, source) {
32
- const result = { ...target };
33
- for (const key of Object.keys(source)) {
34
- const srcVal = source[key];
35
- if (srcVal === void 0 || srcVal === null) continue;
36
- const tgtVal = target[key];
37
- if (typeof srcVal === "object" && !Array.isArray(srcVal) && typeof tgtVal === "object" && tgtVal !== null && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
38
- else result[key] = srcVal;
39
- }
40
- return result;
41
- }
42
- /**
43
- * Load the notification config from the PAI config file.
44
- * Returns defaults merged with any stored values.
45
- */
46
- function loadNotificationConfig() {
47
- let parsed;
48
- try {
49
- parsed = readMainConfigRaw();
50
- } catch {
51
- return { ...DEFAULT_NOTIFICATION_CONFIG };
52
- }
53
- const stored = parsed["notifications"];
54
- if (!stored || typeof stored !== "object") return { ...DEFAULT_NOTIFICATION_CONFIG };
55
- return deepMerge(DEFAULT_NOTIFICATION_CONFIG, stored);
56
- }
57
- /**
58
- * Persist the notification config by merging it into the existing
59
- * CONFIG_FILE. Creates the file if it does not exist.
60
- */
61
- function saveNotificationConfig(config) {
62
- let full = {};
63
- try {
64
- full = readMainConfigRaw();
65
- } catch {}
66
- full["notifications"] = config;
67
- writeMainConfigRaw(full);
68
- }
69
- /**
70
- * Apply a partial update to the current notification config and persist it.
71
- * Returns the new merged config.
72
- */
73
- function patchNotificationConfig(patch) {
74
- const current = loadNotificationConfig();
75
- if (patch.mode !== void 0) current.mode = patch.mode;
76
- if (patch.channels) current.channels = deepMerge(current.channels, patch.channels);
77
- if (patch.routing) current.routing = deepMerge(current.routing, patch.routing);
78
- saveNotificationConfig(current);
79
- return current;
80
- }
81
-
82
- //#endregion
83
- //#region src/workers/keepalive.ts
84
- /**
85
- * keepalive.ts — one cache-keepalive beat, shared by the daemon scheduler and
86
- * the probe script.
87
- *
88
- * The provider (GLM anthropic-compat endpoint) caches the worker prefix
89
- * implicitly — no cache_control blocks — so a trivial single-turn worker spawn
90
- * through the very same runWorker path re-arms that cache for the next real
91
- * worker: identical env, identical tool grants, identical WORKER_CONTRACT_PROMPT
92
- * by construction, because it IS a worker spawn.
93
- *
94
- * Measured 2026-09-18 (docs/cache-keepalive.md): the implicit cache only
95
- * serves spawns within seconds of the previous identical request and never
96
- * survived a 60 s beat cadence in the armed proof — hence the knob defaults
97
- * to off and arming it is the operator's explicit call.
98
- *
99
- * Deliberately NOT a hand-built claude invocation: duplicated prefix
100
- * construction is how this repo has shipped real breakage before (probeResume
101
- * x3, archiver x2). Everything flows through runWorker.
102
- */
103
- /**
104
- * The trivial prompt every cache beat (and probe) carries. Kept in one place
105
- * so the heartbeat arms exactly the prefix a verifying probe measures.
106
- */
107
- const HEARTBEAT_PROMPT = "Reply with exactly one word: pong.";
108
- /** The worker class a beat runs as — the cheapest spawn the config defines. */
109
- const KEEPALIVE_CLASS = "simple";
110
- /** Worker label of every beat, so `pai worker ps` can tell them apart. */
111
- const KEEPALIVE_LABEL = "cache-keepalive";
112
- /** Cadence a keepalive runs at for this config; 0 keeps it off. */
113
- function keepaliveSecs(workers) {
114
- return workers.enabled ? workers.cacheKeepaliveSecs : 0;
115
- }
116
- /**
117
- * The LAST `type:"result"` event of a worker transcript, or null when the
118
- * file is missing, unreadable, or carries no result line (a failed run can
119
- * end without one).
120
- */
121
- function parseLastResultEvent(path) {
122
- let text;
123
- try {
124
- text = readFileSync(path, "utf8");
125
- } catch {
126
- return null;
127
- }
128
- let found = null;
129
- for (const line of text.split("\n")) {
130
- if (!line.startsWith("{")) continue;
131
- let e;
132
- try {
133
- e = JSON.parse(line);
134
- } catch {
135
- continue;
136
- }
137
- if (e.type === "result") found = e;
138
- }
139
- return found;
140
- }
141
- /**
142
- * Metrics of one run from its final result event. Single-turn runs make the
143
- * cumulative result usage identical to first-turn usage, so a warm cache
144
- * shows up directly as cache_read_input_tokens > 0.
145
- */
146
- function extractBeatMetrics(m, ev) {
147
- const u = ev?.usage;
148
- return {
149
- ...m,
150
- input_tokens: u?.input_tokens ?? null,
151
- output_tokens: u?.output_tokens ?? null,
152
- cache_read_input_tokens: u?.cache_read_input_tokens ?? null,
153
- cache_creation_input_tokens: u?.cache_creation_input_tokens ?? null,
154
- duration_api_ms: ev?.duration_api_ms ?? null,
155
- duration_ms: ev?.duration_ms ?? null,
156
- num_turns: ev?.num_turns ?? null
157
- };
158
- }
159
- /** True while a beat's worker is still running — the overlap guard reads it. */
160
- let beatInFlight = false;
161
- /** `keepalive-<YYYYMMDD-HHMMSS>` — beats are minutes apart, seconds suffice. */
162
- function beatId(d = /* @__PURE__ */ new Date()) {
163
- const p = (n) => String(n).padStart(2, "0");
164
- return `keepalive-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
165
- }
166
- /**
167
- * One heartbeat: a single-turn trivial worker through the exact worker spawn
168
- * path, its result metrics parsed from the transcript, one WORKER-KEEPALIVE
169
- * ledger line. Returns null when another beat is still running (overlap
170
- * guard) — a skipped beat is free, a queue of them is not.
171
- *
172
- * The beat runs in the workers logDir: no session renders its status line
173
- * there, so the spawn adopts no spawnerSession and no iTerm identity, and
174
- * worker supervision has no owner to notify — heartbeats are infrastructure,
175
- * not somebody's workers.
176
- */
177
- async function runKeepaliveBeat(opts = {}) {
178
- if (beatInFlight) return null;
179
- beatInFlight = true;
180
- try {
181
- const { workers: config } = readWorkersSection(opts.configPath);
182
- const logDir = workersLogDir(config);
183
- const id = opts.id ?? beatId();
184
- const rc = await runWorker({
185
- claudeArgs: ["-p", HEARTBEAT_PROMPT],
186
- className: KEEPALIVE_CLASS,
187
- label: KEEPALIVE_LABEL,
188
- noPane: true,
189
- quiet: true,
190
- worktreeFlag: false,
191
- id,
192
- cwd: logDir
193
- });
194
- const status = loadStatus(logDir, id);
195
- const metrics = extractBeatMetrics({
196
- id,
197
- ok: rc === 0,
198
- rc,
199
- provider: status?.provider ?? null,
200
- model: status?.model ?? null
201
- }, parseLastResultEvent(eventsPath(logDir, id)));
202
- appendLedger(ledgerPath(logDir), "WORKER-KEEPALIVE", {
203
- id,
204
- provider: metrics.provider,
205
- model: metrics.model,
206
- input_tokens: metrics.input_tokens,
207
- cache_read_input_tokens: metrics.cache_read_input_tokens,
208
- cache_creation_input_tokens: metrics.cache_creation_input_tokens,
209
- duration_api_ms: metrics.duration_api_ms
210
- });
211
- return metrics;
212
- } finally {
213
- beatInFlight = false;
214
- }
215
- }
216
-
217
- //#endregion
218
- //#region src/workers/supervision.ts
219
- /**
220
- * supervision.ts — daemon-side worker supervision, event push not polling.
221
- *
222
- * An orchestrating session must keep an eye on its workers without sleeping
223
- * (the Worker skill forbids busy-waiting), the same way the AIBroker daemon
224
- * keeps an eye on sessions. The PAI daemon already runs all day; this module
225
- * gives it a tick that watches every worker's status file and pushes one-line
226
- * events to the owning session when something happens:
227
- *
228
- * finished — the run ended clean (state done, rc 0)
229
- * failed — the run ended badly (state failed/killed/lost, or rc != 0,
230
- * or the runner pid vanished while state was still running)
231
- * stalled — state running, runner alive, but no new turn for longer than
232
- * the stall threshold (PAI_WORKER_STALL_MINUTES, default 10)
233
- * thrashing — state running, runner alive, turning, but the last N tool
234
- * executions all failed (PAI_WORKER_THRASH_FAILS, default 5) —
235
- * a worker that keeps failing every command never goes quiet,
236
- * so the stall detector alone cannot see it
237
- * sleeping — state running, one Bash command sleeping N >= 60s with nothing
238
- * to wait on — under the stall threshold for runs like
239
- * `sleep 500` (8.3m), which would otherwise be invisible until
240
- * they cross it. The bounded-wait pattern (background a build or
241
- * test, then `sleep 60; ps` cycles) has live children and stays
242
- * quiet; stalled and thrashing remain the backstop for a child
243
- * that never finishes or dies silently
244
- *
245
- * The interactive chat pane (origin "chat") is never supervised: it is the
246
- * operator's own session, not a task worker. Its status file flips to done
247
- * when the session ends, and reporting that as "worker finished" to whoever
248
- * next opens the same tab is noise, not an outcome.
249
- *
250
- * Delivery, in order of preference, never both counted as required:
251
- * 1. AIBroker `send_to_session` into the orchestrator's terminal — the
252
- * one-line Agentish-style notification, arriving as a user turn without
253
- * the orchestrator asking for it. Needs the iTerm identity of the owner,
254
- * which the status line bridges into the session map (see scope.ts).
255
- * 2. An append-only event file, <logDir>/supervision/<owner>.events, one
256
- * JSON line per event. The UserPromptSubmit hook
257
- * (src/hooks/ts/user-prompt/worker-supervision.ts) surfaces undelivered
258
- * lines into the session that owns them, so the events reach the model
259
- * on its next turn even with AIBroker absent.
260
- *
261
- * Exactly-once: every event carries a deterministic id and the set of
262
- * delivered ids per worker is persisted (<logDir>/supervision/state.json), so
263
- * a daemon restart replays nothing. A first run with no state adopts every
264
- * already-terminal worker as delivered — supervision reports transitions it
265
- * observes, not history. Chain stages are separate workers with their own
266
- * status files and are supervised individually.
267
- *
268
- * No model is ever called from here — this is daemon code, and it must stay
269
- * that way: supervision that bills tokens to watch tokens is a bug.
270
- */
271
- /** The kinds that fire mid-run and re-arm on turn progress (see eventId). */
272
- const MID_RUN_KINDS = [
273
- "stalled",
274
- "thrashing",
275
- "sleeping"
276
- ];
277
- /**
278
- * The owning session of a worker, or null when nobody claims it (and so
279
- * nothing to notify): the spawner's claude session id first — that is what
280
- * the hook knows a session by — else the AIBroker iTerm uuid.
281
- */
282
- function ownershipKey(s) {
283
- if (s.spawnerSession) return s.spawnerSession;
284
- if (s.session?.id) return s.session.id;
285
- return null;
286
- }
287
- const TERMINAL = [
288
- "done",
289
- "failed",
290
- "killed",
291
- "lost"
292
- ];
293
- function isTerminal(s) {
294
- return TERMINAL.includes(s.state);
295
- }
296
- /** Age of a "YYYY-MM-DD HH:MM:SS" stamp in ms, NaN-safe (unparsable → 0). */
297
- function ageMs(ts, now) {
298
- const t = Date.parse(ts.replace(" ", "T"));
299
- if (Number.isNaN(t)) return 0;
300
- return Math.max(0, now.getTime() - t);
301
- }
302
- /**
303
- * The stall threshold in minutes from PAI_WORKER_STALL_MINUTES (default 10).
304
- * Anything unparsible or non-positive falls back to the default — a broken
305
- * value must not turn supervision off or make it fire every tick.
306
- */
307
- const DEFAULT_STALL_MINUTES = 10;
308
- function stallMinutesFromEnv(env = process.env) {
309
- const raw = env.PAI_WORKER_STALL_MINUTES;
310
- if (raw === void 0 || raw === "") return DEFAULT_STALL_MINUTES;
311
- const n = Number(raw);
312
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_STALL_MINUTES;
313
- }
314
- /**
315
- * The consecutive-failure threshold from PAI_WORKER_THRASH_FAILS (default 5),
316
- * with the same broken-value tolerance as stallMinutesFromEnv.
317
- */
318
- const DEFAULT_THRASH_FAILS = 5;
319
- function thrashFailsFromEnv(env = process.env) {
320
- const raw = env.PAI_WORKER_THRASH_FAILS;
321
- if (raw === void 0 || raw === "") return DEFAULT_THRASH_FAILS;
322
- const n = Number(raw);
323
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_THRASH_FAILS;
324
- }
325
- /**
326
- * Event id: `<worker>#<kind>` for the once-per-run terminal kinds, and
327
- * `<worker>#<kind>@<turns>` for the mid-run kinds (stalled, thrashing,
328
- * sleeping) — the turn count re-arms the id, so a worker that resumes and
329
- * hits the same condition again gets a second event while a restart cannot
330
- * replay the first.
331
- */
332
- function eventId(worker, kind, turns = 0) {
333
- return MID_RUN_KINDS.includes(kind) ? `${worker}#${kind}@${turns}` : `${worker}#${kind}`;
334
- }
335
- /**
336
- * Live child pids of a runner via `pgrep -P` (its exit 1 means "no
337
- * children"). Any failure — here or in an injected stub — is caught at the
338
- * one call site in detectSupervisionEvents and reads as an empty list, so a
339
- * broken check degrades to firing the sleeping event, never to silencing it.
340
- */
341
- function pgrepChildren(pid) {
342
- return execFileSync("pgrep", ["-P", String(pid)], { encoding: "utf8" }).split("\n").flatMap((l) => {
343
- const n = Number(l.trim());
344
- return Number.isInteger(n) && n > 0 ? [n] : [];
345
- });
346
- }
347
- /** One event's one-liner — the exact line the orchestrator reads. */
348
- function eventText(kind, s, rc, stalledMin) {
349
- const tail = `- see pai worker replay ${s.id}`;
350
- if (kind === "finished") return `worker ${s.id} ${s.label} finished rc=0 ${tail}`;
351
- if (kind === "stalled") return `worker ${s.id} ${s.label} stalled ${stalledMin}m no turns ${tail}`;
352
- if (kind === "thrashing") return `worker ${s.id} ${s.label} thrashing: ${s.consecFails} consecutive failed commands ${tail}`;
353
- if (kind === "sleeping") return `worker ${s.id} ${s.label} sleeping ${s.sleepSec}s in one command ${tail}`;
354
- const why = rc === null ? "runner gone" : `rc=${rc}`;
355
- return `worker ${s.id} ${s.label} failed ${why} ${tail}`;
356
- }
357
- /**
358
- * Detect conditions across a ledger snapshot. Pure: statuses in, events out;
359
- * dedup against delivered ids is the caller's job (see filterUndelivered).
360
- * Workers without an owning session are skipped — nobody to notify.
361
- */
362
- function detectSupervisionEvents(statuses, opts) {
363
- const isAlive = opts.isAlive ?? alive;
364
- const deadGraceMs = opts.deadGraceMs ?? 15e3;
365
- const out = [];
366
- for (const s of statuses) {
367
- const session = ownershipKey(s);
368
- if (!session) continue;
369
- if (isChatPane(s)) continue;
370
- let kind = null;
371
- let rc = null;
372
- let stalledMin = null;
373
- if (isTerminal(s)) if (s.state === "done" && (s.rc ?? 0) === 0) {
374
- kind = "finished";
375
- rc = 0;
376
- } else {
377
- kind = "failed";
378
- rc = s.state === "lost" ? null : s.rc;
379
- }
380
- else if (!isAlive(s.pid) && ageMs(s.updated, opts.now) > deadGraceMs) {
381
- kind = "failed";
382
- rc = null;
383
- } else if (ageMs(s.updated, opts.now) > opts.stallMs) {
384
- kind = "stalled";
385
- stalledMin = Math.floor(ageMs(s.updated, opts.now) / 6e4);
386
- } else if ((s.consecFails ?? 0) >= (opts.thrashFails ?? DEFAULT_THRASH_FAILS)) kind = "thrashing";
387
- else if (s.sleepSec != null) {
388
- let hasChildren = false;
389
- if (isAlive(s.pid)) try {
390
- hasChildren = (opts.childPids ?? pgrepChildren)(s.pid).length > 0;
391
- } catch {
392
- hasChildren = false;
393
- }
394
- if (!hasChildren) kind = "sleeping";
395
- }
396
- if (!kind) continue;
397
- out.push({
398
- id: eventId(s.id, kind, s.turns),
399
- ts: nowStamp(opts.now),
400
- kind,
401
- worker: s.id,
402
- label: s.label,
403
- session,
404
- rc,
405
- stalledMin,
406
- ...kind === "thrashing" ? { consecFails: s.consecFails ?? 0 } : {},
407
- ...kind === "sleeping" ? { sleepSec: s.sleepSec ?? 0 } : {},
408
- text: eventText(kind, s, rc, stalledMin)
409
- });
410
- }
411
- return out;
412
- }
413
- /** Cap on remembered workers: terminal and delivered is history, not state. */
414
- const MAX_REMEMBERED_WORKERS = 500;
415
- function supervisionDirPath(logDir) {
416
- return join(logDir, "supervision");
417
- }
418
- function supervisionStatePath(logDir) {
419
- return join(supervisionDirPath(logDir), "state.json");
420
- }
421
- function supervisionEventsPath(logDir, session) {
422
- return join(supervisionDirPath(logDir), `${session}.events`);
423
- }
424
- /** Receipts of events already pushed into a terminal, one event id per line. */
425
- function supervisionPushedPath(logDir, session) {
426
- return join(supervisionDirPath(logDir), `${session}.pushed`);
427
- }
428
- function loadSupervisionState(path) {
429
- if (!existsSync(path)) return {
430
- state: { delivered: {} },
431
- fresh: true
432
- };
433
- try {
434
- const parsed = JSON.parse(readFileSync(path, "utf8"));
435
- if (parsed && typeof parsed === "object" && typeof parsed.delivered === "object") return {
436
- state: parsed,
437
- fresh: false
438
- };
439
- } catch {}
440
- return {
441
- state: { delivered: {} },
442
- fresh: true
443
- };
444
- }
445
- function saveSupervisionState(path, state) {
446
- const dir = dirname(path);
447
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
448
- const tmp = `${path}.tmp`;
449
- writeFileSync(tmp, JSON.stringify(state), "utf8");
450
- renameSync(tmp, path);
451
- }
452
- /**
453
- * Drop the delivered ids of workers the ledger no longer lists (absent means
454
- * history: their status files are gone) and cap only that history — newest
455
- * ids last, worker ids sort chronologically — so the persisted state stays
456
- * bounded. A worker the ledger still lists must never lose its receipts:
457
- * pruning one un-marks its terminal events as delivered, and every save
458
- * re-arms them into an unbounded re-delivery loop (the 2026-09-25 incident:
459
- * one old chain stage re-pushed 7,393 times, because the cap of 500 bit into
460
- * a ledger of ~1,700 live statuses).
461
- */
462
- function pruneState(state, statuses, maxAbsent = MAX_REMEMBERED_WORKERS) {
463
- const present = new Set(statuses.map((s) => s.id));
464
- const entries = Object.entries(state.delivered);
465
- const byId = ([a], [b]) => a < b ? -1 : a > b ? 1 : 0;
466
- const keptPresent = entries.filter(([id]) => present.has(id)).sort(byId);
467
- const absent = entries.filter(([id]) => !present.has(id)).sort(byId);
468
- const keptAbsent = absent.slice(Math.max(0, absent.length - maxAbsent));
469
- return { delivered: Object.fromEntries([...keptAbsent, ...keptPresent]) };
470
- }
471
- /**
472
- * First run: adopt every already-terminal worker as delivered. Supervision
473
- * reports the transitions it observes from now on, never a replay of every
474
- * worker that ever finished before it started.
475
- */
476
- function baselineState(statuses) {
477
- const delivered = {};
478
- for (const s of statuses) {
479
- if (!isTerminal(s) || !ownershipKey(s)) continue;
480
- delivered[s.id] = [eventId(s.id, "finished"), eventId(s.id, "failed")];
481
- }
482
- return { delivered };
483
- }
484
- /** The events whose ids the state has not seen yet. */
485
- function filterUndelivered(events, state) {
486
- return events.filter((e) => !(state.delivered[e.worker] ?? []).includes(e.id));
487
- }
488
- function markDelivered(state, events) {
489
- for (const e of events) {
490
- const ids = state.delivered[e.worker] ?? [];
491
- if (!ids.includes(e.id)) ids.push(e.id);
492
- state.delivered[e.worker] = ids;
493
- }
494
- }
495
- /**
496
- * Default push: AIBroker `send_to_session` into the orchestrator's terminal,
497
- * using the iTerm uuid the worker recorded (`session.id`) or the one the
498
- * session map bridges for the spawner's claude session. A timeout still
499
- * delivered (see aibroker-client) counts as delivered; every failure is
500
- * reported as not-delivered so only the event file carries the event.
501
- */
502
- const aibrokerPush = async (logDir, target, ev) => {
503
- const iterm = target.iterm ?? (target.claudeSession ? itermForClaudeSession(logDir, target.claudeSession) : null);
504
- if (!iterm) return false;
505
- try {
506
- const { sendToSession } = await import("./aibroker-client-B8c42Lh8.mjs");
507
- const r = await sendToSession(iterm, ev.text);
508
- return r.ok || r.timedOut === true;
509
- } catch {
510
- return false;
511
- }
512
- };
513
- /**
514
- * Whether an event should be relayed into the owner's terminal. The mid-run
515
- * kinds (stalled, thrashing, sleeping) ALWAYS relay: a json/stream-json
516
- * caller knows the run's result at exit but has no other way to hear
517
- * mid-run trouble. Only the terminal kinds (finished/failed) skip the relay
518
- * for those callers — they read the run's result itself, and the harness
519
- * already notifies them on process exit, so the one-liner would cost a full
520
- * extra turn for information they already have. Interactive/pane launches
521
- * and the default text format are never skipped.
522
- */
523
- function shouldRelay(kind, outputFormat) {
524
- if (MID_RUN_KINDS.includes(kind)) return true;
525
- return outputFormat !== "json" && outputFormat !== "stream-json";
526
- }
527
- function appendEventFile(logDir, ev) {
528
- const path = supervisionEventsPath(logDir, ev.session);
529
- const dir = supervisionDirPath(logDir);
530
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
531
- appendFileSync(path, JSON.stringify(ev) + "\n", "utf8");
532
- }
533
- /** Record that an event reached its owner's terminal — the hook then skips it. */
534
- function appendPushReceipt(logDir, ev) {
535
- try {
536
- appendFileSync(supervisionPushedPath(logDir, ev.session), `${ev.id}\n`, "utf8");
537
- } catch {}
538
- }
539
- /**
540
- * One supervision pass over the ledger: detect, append each new event to its
541
- * owner's event file, push it best effort, persist the delivered ids. The
542
- * event file is the durable record — an event is marked delivered only after
543
- * its line landed there, so a failed tick retries, a repeated one replays
544
- * nothing.
545
- */
546
- async function runSupervisionTick(logDir, opts = {}) {
547
- const now = opts.now ?? /* @__PURE__ */ new Date();
548
- const statuses = opts.statuses ?? loadStatuses(logDir);
549
- const statePath = supervisionStatePath(logDir);
550
- const loaded = loadSupervisionState(statePath);
551
- let state = loaded.state;
552
- if (loaded.fresh) state = baselineState(statuses);
553
- const pending = filterUndelivered(detectSupervisionEvents(statuses, {
554
- stallMs: opts.stallMs ?? DEFAULT_STALL_MINUTES * 6e4,
555
- thrashFails: opts.thrashFails ?? thrashFailsFromEnv(),
556
- now,
557
- ...opts.isAlive ? { isAlive: opts.isAlive } : {}
558
- }), state);
559
- const written = [];
560
- for (const ev of pending) {
561
- appendEventFile(logDir, ev);
562
- written.push(ev);
563
- }
564
- const push = opts.push ?? aibrokerPush;
565
- const pushed = [];
566
- for (const ev of written) {
567
- const owner = statuses.find((s) => s.id === ev.worker);
568
- if (!shouldRelay(ev.kind, owner?.outputFormat)) continue;
569
- const target = {
570
- claudeSession: owner?.spawnerSession ?? null,
571
- iterm: owner?.session?.id ?? null
572
- };
573
- let ok = false;
574
- try {
575
- ok = await push(logDir, target, ev);
576
- } catch {
577
- ok = false;
578
- }
579
- if (ok) {
580
- pushed.push(ev);
581
- appendPushReceipt(logDir, ev);
582
- }
583
- }
584
- markDelivered(state, written);
585
- saveSupervisionState(statePath, pruneState(state, statuses));
586
- return {
587
- events: written,
588
- pushed
589
- };
590
- }
591
-
592
- //#endregion
593
- //#region src/daemon/daemon/scheduler.ts
594
- /**
595
- * Index, embed, vault index, and registry scan schedulers for the PAI daemon.
596
- * Exports run* functions (also called on-demand by the IPC handler)
597
- * and the start* functions invoked once at daemon startup.
598
- */
599
- /** Minimum interval between vault index runs (30 minutes). */
600
- const VAULT_INDEX_MIN_INTERVAL_MS = 1800 * 1e3;
601
- /**
602
- * Run a full index pass. Guards against overlapping runs with indexInProgress.
603
- * Called both by the scheduler and by the index_now IPC method.
604
- */
605
- async function runIndex() {
606
- if (indexInProgress) {
607
- process.stderr.write("[pai-daemon] Index already in progress, skipping.\n");
608
- return;
609
- }
610
- if (embedInProgress) {
611
- process.stderr.write("[pai-daemon] Embed in progress, deferring index run.\n");
612
- return;
613
- }
614
- setIndexInProgress(true);
615
- const t0 = Date.now();
616
- try {
617
- process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
618
- const { projects, result } = await storageBackend.indexAll(registryBackend);
619
- const elapsed = Date.now() - t0;
620
- setLastIndexTime(Date.now());
621
- process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
622
- } catch (e) {
623
- const msg = e instanceof Error ? e.message : String(e);
624
- process.stderr.write(`[pai-daemon] Index error: ${msg}\n`);
625
- } finally {
626
- setIndexInProgress(false);
627
- }
628
- }
629
- /**
630
- * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.
631
- * Skips if no vaultPath is configured, or if project index/embed is in progress.
632
- */
633
- async function runVaultIndex() {
634
- if (!daemonConfig.vaultPath) return;
635
- if (vaultIndexInProgress) {
636
- process.stderr.write("[pai-daemon] Vault index already in progress, skipping.\n");
637
- return;
638
- }
639
- if (indexInProgress || embedInProgress) {
640
- process.stderr.write("[pai-daemon] Index/embed in progress, deferring vault index.\n");
641
- return;
642
- }
643
- const { lastVaultIndexTime } = await import("./state-DW8zdweW.mjs");
644
- if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) return;
645
- let vaultProjectId = daemonConfig.vaultProjectId;
646
- if (!vaultProjectId) {
647
- const row = await registryBackend.getProjectByRootPath(daemonConfig.vaultPath);
648
- vaultProjectId = row?.id ?? 999;
649
- if (!row) process.stderr.write("[pai-daemon] Vault not in project registry — using synthetic project ID 999.\n");
650
- }
651
- setVaultIndexInProgress(true);
652
- const t0 = Date.now();
653
- process.stderr.write("[pai-daemon] Starting vault index run...\n");
654
- try {
655
- const { indexVault } = await import("./vault-indexer-BG1sEVyM.mjs");
656
- const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath);
657
- const elapsed = Date.now() - t0;
658
- setLastVaultIndexTime(Date.now());
659
- process.stderr.write(`[pai-daemon] Vault index complete: ${r.filesIndexed} files, ${r.linksExtracted} links, ${r.deadLinksFound} dead, ${r.orphansFound} orphans (${elapsed}ms)\n`);
660
- } catch (e) {
661
- const msg = e instanceof Error ? e.message : String(e);
662
- process.stderr.write(`[pai-daemon] Vault index error: ${msg}\n`);
663
- } finally {
664
- setVaultIndexInProgress(false);
665
- }
666
- }
667
- /**
668
- * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.
669
- */
670
- function startIndexScheduler() {
671
- const intervalMs = daemonConfig.indexIntervalSecs * 1e3;
672
- process.stderr.write(`[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\n`);
673
- const cycle = (label, withEmbed) => runIndex().then(() => runVaultIndex()).then(() => withEmbed ? runEmbed() : void 0).catch((e) => {
674
- process.stderr.write(`[pai-daemon] ${label} index error: ${e}\n`);
675
- });
676
- if (daemonConfig.maintenanceHour === void 0) setTimeout(() => void cycle("Startup", daemonConfig.embedOnStartup), 2e3);
677
- else process.stderr.write("[pai-daemon] Startup index pass skipped (maintenanceHour is set).\n");
678
- const firstDelayMs = msUntilNextAnchor(daemonConfig.maintenanceHour, intervalMs);
679
- 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`);
680
- let timer;
681
- const startTimer = () => {
682
- timer = setInterval(() => void cycle("Scheduled", true), intervalMs);
683
- if (timer.unref) timer.unref();
684
- setIndexSchedulerTimer(timer);
685
- };
686
- const first = setTimeout(() => {
687
- cycle("Scheduled", true);
688
- startTimer();
689
- }, firstDelayMs);
690
- if (first.unref) first.unref();
691
- }
692
- /**
693
- * Milliseconds until the next occurrence of `hour`:00 local time.
694
- * Falls back to the plain interval when no anchor hour is configured.
695
- */
696
- function msUntilNextAnchor(hour, intervalMs) {
697
- if (hour === void 0 || !Number.isInteger(hour) || hour < 0 || hour > 23) return intervalMs;
698
- const now = /* @__PURE__ */ new Date();
699
- const next = new Date(now);
700
- next.setHours(hour, 0, 0, 0);
701
- if (next <= now) next.setDate(next.getDate() + 1);
702
- return next.getTime() - now.getTime();
703
- }
704
- /**
705
- * Run an embedding pass for all unembedded chunks (Postgres backend only).
706
- */
707
- async function runEmbed() {
708
- if (embedInProgress) {
709
- process.stderr.write("[pai-daemon] Embed already in progress, skipping.\n");
710
- return;
711
- }
712
- if (indexInProgress) {
713
- process.stderr.write("[pai-daemon] Index in progress, deferring embed pass.\n");
714
- return;
715
- }
716
- if (!storageBackend.supportsPostgresFeatures) return;
717
- setEmbedInProgress(true);
718
- const t0 = Date.now();
719
- try {
720
- process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
721
- const projectNames = /* @__PURE__ */ new Map();
722
- try {
723
- const rows = await registryBackend.listProjects({ status: "active" });
724
- for (const r of rows) projectNames.set(r.id, r.slug);
725
- } catch {}
726
- const { embedChunksWithBackend } = await import("./indexer-backend-DFF2FrYx.mjs");
727
- const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames, { maxMillis: 24e4 });
728
- const elapsed = Date.now() - t0;
729
- setLastEmbedTime(Date.now());
730
- process.stderr.write(`[pai-daemon] Embed pass complete: ${count} chunks embedded (${elapsed}ms)\n`);
731
- } catch (e) {
732
- const msg = e instanceof Error ? e.message : String(e);
733
- process.stderr.write(`[pai-daemon] Embed error: ${msg}\n`);
734
- } finally {
735
- setEmbedInProgress(false);
736
- }
737
- }
738
- /**
739
- * Start the periodic embed scheduler. Initial run is 60 seconds after startup.
740
- */
741
- function startEmbedScheduler() {
742
- const intervalMs = daemonConfig.embedIntervalSecs * 1e3;
743
- process.stderr.write(`[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\n`);
744
- if (daemonConfig.embedOnStartup) setTimeout(() => {
745
- runEmbed().catch((e) => {
746
- process.stderr.write(`[pai-daemon] Startup embed error: ${e}\n`);
747
- });
748
- }, 6e4);
749
- else process.stderr.write("[pai-daemon] Startup embed pass skipped (embedOnStartup=false).\n");
750
- if (daemonConfig.maintenanceHour !== void 0) {
751
- process.stderr.write("[pai-daemon] Standalone embed timer not armed (maintenanceHour is set; embed runs at the end of each anchored index cycle).\n");
752
- return;
753
- }
754
- const timer = setInterval(() => {
755
- runEmbed().catch((e) => {
756
- process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\n`);
757
- });
758
- }, intervalMs);
759
- if (timer.unref) timer.unref();
760
- setEmbedSchedulerTimer(timer);
761
- }
762
- const REGISTRY_SCAN_INTERVAL_MS = 1800 * 1e3;
763
- const REGISTRY_SCAN_STARTUP_DELAY_MS = 1e4;
764
- /**
765
- * Start the periodic registry scan scheduler.
766
- * First scan runs 10 seconds after startup (enough time for IPC to stabilise).
767
- * Subsequent scans run every 30 minutes.
768
- * Uses the work-queue so the scan runs on the worker thread without blocking IPC.
769
- */
770
- function startRegistryScanScheduler() {
771
- process.stderr.write("[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\n");
772
- setTimeout(() => {
773
- import("./work-queue-worker-rFZvTO3p.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
774
- process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\n`);
775
- });
776
- }, REGISTRY_SCAN_STARTUP_DELAY_MS);
777
- const timer = setInterval(() => {
778
- import("./work-queue-worker-rFZvTO3p.mjs").then(({ enqueueRegistryScan }) => enqueueRegistryScan()).catch((e) => {
779
- process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\n`);
780
- });
781
- }, REGISTRY_SCAN_INTERVAL_MS);
782
- if (timer.unref) timer.unref();
783
- }
784
- /** How often the daemon looks at the worker ledger for supervision events. */
785
- const SUPERVISION_TICK_MS = 3e4;
786
- /** First supervision pass waits for the first statuses to exist at all. */
787
- const SUPERVISION_STARTUP_DELAY_MS = 15e3;
788
- /**
789
- * Start the worker supervisor: one tick every 30 s over the workers ledger
790
- * (src/workers/supervision.ts) that pushes finished/failed/stalled events to
791
- * the owning session — the daemon-side replacement for orchestrator polling.
792
- * Daemon code only: no model call has any business being here, and none is
793
- * made. Disabled workers disable the supervisor with it.
794
- */
795
- function startWorkerSupervisor() {
796
- let enabled = false;
797
- try {
798
- enabled = readWorkersSection().workers.enabled;
799
- } catch (e) {
800
- process.stderr.write(`[pai-daemon] Worker supervisor: not started (${e instanceof Error ? e.message : String(e)})\n`);
801
- return;
802
- }
803
- if (!enabled) {
804
- process.stderr.write("[pai-daemon] Worker supervisor: disabled (workers are off)\n");
805
- return;
806
- }
807
- const tick = () => {
808
- runSupervisionTick(workersLogDir(readWorkersSection().workers), {
809
- stallMs: stallMinutesFromEnv() * 6e4,
810
- thrashFails: thrashFailsFromEnv()
811
- }).then((r) => {
812
- for (const ev of r.events) process.stderr.write(`[pai-daemon] Supervision: ${ev.text}\n`);
813
- }).catch((e) => {
814
- process.stderr.write(`[pai-daemon] Supervision tick error: ${e instanceof Error ? e.message : String(e)}\n`);
815
- });
816
- };
817
- const first = setTimeout(tick, SUPERVISION_STARTUP_DELAY_MS);
818
- if (first.unref) first.unref();
819
- const timer = setInterval(tick, SUPERVISION_TICK_MS);
820
- if (timer.unref) timer.unref();
821
- process.stderr.write(`[pai-daemon] Worker supervisor: every ${Math.round(SUPERVISION_TICK_MS / 1e3)}s\n`);
822
- }
823
- /** First beat waits a while after boot; the daemon start is itself a warm request. */
824
- const KEEPALIVE_STARTUP_DELAY_MS = 6e4;
825
- /**
826
- * Start the cache keepalive: every workers.cacheKeepaliveSecs one trivial
827
- * single-turn worker (src/workers/keepalive.ts) re-arms the provider's
828
- * implicit prompt cache so real worker spawns start warm. Needs no storage
829
- * backend — it starts with the IPC server, not after the federation connect.
830
- * 0 (or workers off) keeps it off. A failed beat is logged and swallowed.
831
- */
832
- function startCacheKeepalive(opts = {}) {
833
- let workers;
834
- try {
835
- workers = readWorkersSection(opts.configPath).workers;
836
- } catch (e) {
837
- process.stderr.write(`[pai-daemon] Cache keepalive: not started (${e instanceof Error ? e.message : String(e)})\n`);
838
- return;
839
- }
840
- if (!workers.enabled) {
841
- process.stderr.write("[pai-daemon] Cache keepalive: disabled (workers are off)\n");
842
- return;
843
- }
844
- const secs = keepaliveSecs(workers);
845
- if (!secs) {
846
- process.stderr.write("[pai-daemon] Cache keepalive: disabled (cacheKeepaliveSecs is 0)\n");
847
- return;
848
- }
849
- let notedNoPostgres = false;
850
- const supportsObservations = () => {
851
- if (storageBackend?.supportsPostgresFeatures) return true;
852
- if (!notedNoPostgres) {
853
- notedNoPostgres = true;
854
- process.stderr.write("[pai-daemon] Cache keepalive: observation rows need Postgres — ledger line only\n");
855
- }
856
- return false;
857
- };
858
- const beat = async () => {
859
- const m = await (opts.beat ?? runKeepaliveBeat)();
860
- if (!m) return;
861
- if (!supportsObservations()) return;
862
- try {
863
- await storageBackend.storeObservationWithProject(registryBackend, {
864
- session_id: m.id,
865
- type: "change",
866
- title: `cache keepalive ${m.ok ? "ok" : "failed"} (cache_read=${m.cache_read_input_tokens}, api_ms=${m.duration_api_ms})`,
867
- tool_name: "cache-keepalive",
868
- 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}`,
869
- tool_input_summary: void 0,
870
- files_read: [],
871
- files_modified: [],
872
- concepts: []
873
- });
874
- } catch (e) {
875
- process.stderr.write(`[pai-daemon] Cache keepalive observation error: ${e instanceof Error ? e.message : String(e)}\n`);
876
- }
877
- };
878
- const first = setTimeout(() => {
879
- beat().catch((e) => {
880
- process.stderr.write(`[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\n`);
881
- });
882
- }, KEEPALIVE_STARTUP_DELAY_MS);
883
- if (first.unref) first.unref();
884
- const timer = setInterval(() => {
885
- beat().catch((e) => {
886
- process.stderr.write(`[pai-daemon] Cache keepalive beat error: ${e instanceof Error ? e.message : String(e)}\n`);
887
- });
888
- }, secs * 1e3);
889
- if (timer.unref) timer.unref();
890
- setCacheKeepaliveTimer(timer);
891
- process.stderr.write(`[pai-daemon] Cache keepalive: every ${secs}s\n`);
892
- }
893
- /** Tick cadence: coarse enough to be cheap, fine enough that a session
894
- * crossing idleMinutes gets beaten within a minute of becoming eligible. */
895
- const SESSION_KEEPALIVE_TICK_MS = 6e4;
896
- /**
897
- * Start the interactive-session cache keepalive: every 60s, beat every live
898
- * session that has gone idle past `sessions.cacheKeepalive.idleMinutes`
899
- * (src/daemon/session-keepalive.ts). Off unless
900
- * `sessions.cacheKeepalive.enabled` is true — no hot-reload, matching
901
- * startCacheKeepalive above: config changes need a daemon restart.
902
- */
903
- function startSessionKeepalive() {
904
- const config = daemonConfig.sessions.cacheKeepalive;
905
- if (!config.enabled) {
906
- process.stderr.write("[pai-daemon] Session keepalive: disabled (sessions.cacheKeepalive.enabled is false)\n");
907
- return;
908
- }
909
- const tick = () => {
910
- runSessionKeepaliveTick(config).catch((e) => {
911
- process.stderr.write(`[pai-daemon] Session keepalive tick error: ${e instanceof Error ? e.message : String(e)}\n`);
912
- });
913
- };
914
- const timer = setInterval(tick, SESSION_KEEPALIVE_TICK_MS);
915
- if (timer.unref) timer.unref();
916
- process.stderr.write(`[pai-daemon] Session keepalive: every ${SESSION_KEEPALIVE_TICK_MS / 1e3}s (idle >= ${config.idleMinutes}min, hours ${config.activeHours}, max ${config.maxBeats} beats/stretch)\n`);
917
- }
918
-
919
- //#endregion
920
- //#region src/daemon/daemon/dispatcher.ts
921
- /**
922
- * Tool dispatcher — maps IPC method names to PAI tool functions.
923
- */
924
- /**
925
- * Dispatch an IPC tool call to the appropriate tool function.
926
- * Returns the tool result or throws on unknown/failed methods.
927
- */
928
- async function dispatchTool(method, params) {
929
- const p = params;
930
- switch (method) {
931
- case "memory_search": return toolMemorySearch(registryBackend, storageBackend, p);
932
- case "memory_get": return toolMemoryGet(registryBackend, p);
933
- case "memory_outline": return toolMemoryOutline(registryBackend, p);
934
- case "project_info": return toolProjectInfo(registryBackend, p);
935
- case "project_list": return toolProjectList(registryBackend, p);
936
- case "session_list": return toolSessionList(registryBackend, p);
937
- case "registry_search": return toolRegistrySearch(registryBackend, p);
938
- case "project_detect": return toolProjectDetect(p);
939
- case "project_health": return toolProjectHealth(registryBackend, p);
940
- case "project_todo": return toolProjectTodo(registryBackend, p);
941
- case "memory_wakeup": return toolMemoryWakeup(registryBackend, p);
942
- case "memory_taxonomy": return toolMemoryTaxonomy(registryBackend, storageBackend, p);
943
- case "topic_check": return detectTopicShift(registryBackend, storageBackend, p);
944
- case "session_auto_route": return toolSessionRoute(registryBackend, storageBackend, p);
945
- case "zettel_explore":
946
- case "zettel_health":
947
- case "zettel_surprise":
948
- case "zettel_suggest":
949
- case "zettel_converse":
950
- case "zettel_themes":
951
- case "zettel_god_notes":
952
- case "zettel_communities": {
953
- const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import("./tools-C-_l0o8Z.mjs");
954
- switch (method) {
955
- case "zettel_explore": return toolZettelExplore(storageBackend, p);
956
- case "zettel_health": return toolZettelHealth(storageBackend, p);
957
- case "zettel_surprise": return toolZettelSurprise(storageBackend, p);
958
- case "zettel_suggest": return toolZettelSuggest(storageBackend, p);
959
- case "zettel_converse": return toolZettelConverse(storageBackend, p);
960
- case "zettel_themes": return toolZettelThemes(storageBackend, p);
961
- case "zettel_god_notes": return toolZettelGodNotes(storageBackend, p);
962
- case "zettel_communities": return toolZettelCommunities(storageBackend, p);
963
- }
964
- break;
965
- }
966
- case "graph_clusters": {
967
- const { handleGraphClusters } = await import("./clusters-BCtD3fbe.mjs");
968
- return handleGraphClusters(storageBackend, p);
969
- }
970
- case "graph_neighborhood": {
971
- const { handleGraphNeighborhood } = await import("./neighborhood-2FsmzoxG.mjs");
972
- return handleGraphNeighborhood(storageBackend, p);
973
- }
974
- case "graph_note_context": {
975
- const { handleGraphNoteContext } = await import("./note-context-De63k8na.mjs");
976
- return handleGraphNoteContext(storageBackend, p);
977
- }
978
- case "graph_trace": {
979
- const { handleGraphTrace } = await import("./trace-CtV0RO6n.mjs");
980
- return handleGraphTrace(storageBackend, p);
981
- }
982
- case "graph_latent_ideas": {
983
- const { handleGraphLatentIdeas } = await import("./latent-ideas-wwAXquGe.mjs");
984
- return handleGraphLatentIdeas(storageBackend, p);
985
- }
986
- case "idea_materialize": {
987
- const { handleIdeaMaterialize } = await import("./latent-ideas-wwAXquGe.mjs");
988
- if (!daemonConfig.vaultPath) throw new Error("idea_materialize requires vaultPath to be configured in the daemon config");
989
- return handleIdeaMaterialize(p, daemonConfig.vaultPath);
990
- }
991
- case "kg_add":
992
- case "kg_query":
993
- case "kg_invalidate":
994
- case "kg_contradictions": {
995
- const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import("./tools-C-_l0o8Z.mjs");
996
- if (!storageBackend.supportsPostgresFeatures) throw new Error(`${method} requires a Postgres storage backend`);
997
- switch (method) {
998
- case "kg_add": return toolKgAdd(storageBackend, p);
999
- case "kg_query": return toolKgQuery(storageBackend, p);
1000
- case "kg_invalidate": return toolKgInvalidate(storageBackend, p);
1001
- case "kg_contradictions": return toolKgContradictions(storageBackend, p);
1002
- }
1003
- break;
1004
- }
1005
- case "memory_tunnels": {
1006
- const { toolMemoryTunnels } = await import("./tools-C-_l0o8Z.mjs");
1007
- return toolMemoryTunnels(registryBackend, storageBackend, p);
1008
- }
1009
- case "memory_feedback": return await toolMemoryFeedback(storageBackend, p);
1010
- case "memory_kg_search":
1011
- if (!storageBackend.supportsPostgresFeatures) throw new Error("memory_kg_search requires a Postgres storage backend for KG triple expansion");
1012
- return await toolMemoryKgSearch(storageBackend, p);
1013
- default: throw new Error(`Unknown method: ${method}`);
1014
- }
1015
- }
1016
-
1017
- //#endregion
1018
- //#region src/daemon/daemon/handler.ts
1019
- function sendResponse(socket, response) {
1020
- try {
1021
- socket.write(JSON.stringify(response) + "\n");
1022
- } catch {}
1023
- }
1024
- /**
1025
- * Handle a single IPC request.
1026
- */
1027
- async function handleRequest(request, socket) {
1028
- const { id, method, params } = request;
1029
- if (method === "status") {
1030
- const dbStats = await (async () => {
1031
- try {
1032
- if (!storageBackend) return null;
1033
- const fedStats = await storageBackend.getStats();
1034
- const projects = await registryBackend.countProjects();
1035
- return {
1036
- files: fedStats.files,
1037
- chunks: fedStats.chunks,
1038
- projects
1039
- };
1040
- } catch {
1041
- return null;
1042
- }
1043
- })();
1044
- sendResponse(socket, {
1045
- id,
1046
- ok: true,
1047
- result: {
1048
- uptime: Math.floor((Date.now() - startTime) / 1e3),
1049
- indexInProgress,
1050
- lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,
1051
- indexIntervalSecs: daemonConfig.indexIntervalSecs,
1052
- embedInProgress,
1053
- lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,
1054
- embedIntervalSecs: daemonConfig.embedIntervalSecs,
1055
- socketPath: daemonConfig.socketPath,
1056
- storageBackend: storageBackend?.backendType ?? "initializing",
1057
- db: dbStats,
1058
- vaultIndexInProgress,
1059
- lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,
1060
- vaultPath: daemonConfig.vaultPath ?? null,
1061
- workQueue: getStats(),
1062
- backendOutage: getBackendOutage()
1063
- }
1064
- });
1065
- socket.end();
1066
- return;
1067
- }
1068
- if (method === "index_now") {
1069
- runIndex().catch((e) => {
1070
- process.stderr.write(`[pai-daemon] index_now error: ${e}\n`);
1071
- });
1072
- sendResponse(socket, {
1073
- id,
1074
- ok: true,
1075
- result: { triggered: true }
1076
- });
1077
- socket.end();
1078
- return;
1079
- }
1080
- if (method === "vault_index_now") {
1081
- runVaultIndex().catch((e) => {
1082
- process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\n`);
1083
- });
1084
- sendResponse(socket, {
1085
- id,
1086
- ok: true,
1087
- result: { triggered: true }
1088
- });
1089
- socket.end();
1090
- return;
1091
- }
1092
- if (method === "notification_get_config") {
1093
- sendResponse(socket, {
1094
- id,
1095
- ok: true,
1096
- result: {
1097
- config: notificationConfig,
1098
- activeChannels: Object.entries(notificationConfig.channels).filter(([ch, cfg]) => ch !== "voice" && cfg.enabled).map(([ch]) => ch)
1099
- }
1100
- });
1101
- socket.end();
1102
- return;
1103
- }
1104
- if (method === "notification_set_config") {
1105
- try {
1106
- const p = params;
1107
- const updated = patchNotificationConfig({
1108
- mode: p.mode,
1109
- channels: p.channels,
1110
- routing: p.routing
1111
- });
1112
- setNotificationConfig(updated);
1113
- sendResponse(socket, {
1114
- id,
1115
- ok: true,
1116
- result: { config: updated }
1117
- });
1118
- } catch (e) {
1119
- sendResponse(socket, {
1120
- id,
1121
- ok: false,
1122
- error: e instanceof Error ? e.message : String(e)
1123
- });
1124
- }
1125
- socket.end();
1126
- return;
1127
- }
1128
- if (method === "notification_send") {
1129
- const p = params;
1130
- if (!p.message) {
1131
- sendResponse(socket, {
1132
- id,
1133
- ok: false,
1134
- error: "notification_send: message is required"
1135
- });
1136
- socket.end();
1137
- return;
1138
- }
1139
- routeNotification({
1140
- event: p.event ?? "info",
1141
- message: p.message,
1142
- title: p.title
1143
- }, notificationConfig).then((result) => {
1144
- sendResponse(socket, {
1145
- id,
1146
- ok: true,
1147
- result
1148
- });
1149
- socket.end();
1150
- }).catch((e) => {
1151
- sendResponse(socket, {
1152
- id,
1153
- ok: false,
1154
- error: e instanceof Error ? e.message : String(e)
1155
- });
1156
- socket.end();
1157
- });
1158
- return;
1159
- }
1160
- if (method === "observation_store") {
1161
- if (!storageBackend?.supportsPostgresFeatures) {
1162
- sendResponse(socket, {
1163
- id,
1164
- ok: false,
1165
- error: "Observations require Postgres backend"
1166
- });
1167
- socket.end();
1168
- return;
1169
- }
1170
- try {
1171
- const p = params;
1172
- sendResponse(socket, {
1173
- id,
1174
- ok: true,
1175
- result: {
1176
- ok: true,
1177
- id: await storageBackend.storeObservationWithProject(registryBackend, {
1178
- session_id: p.session_id,
1179
- type: p.type,
1180
- title: p.title,
1181
- narrative: p.narrative ?? null,
1182
- tool_name: p.tool_name,
1183
- tool_input_summary: p.tool_input_summary ?? void 0,
1184
- files_read: p.files_read ?? [],
1185
- files_modified: p.files_modified ?? [],
1186
- concepts: p.concepts ?? [],
1187
- cwd: p.cwd
1188
- })
1189
- }
1190
- });
1191
- } catch (e) {
1192
- sendResponse(socket, {
1193
- id,
1194
- ok: false,
1195
- error: e instanceof Error ? e.message : String(e)
1196
- });
1197
- }
1198
- socket.end();
1199
- return;
1200
- }
1201
- if (method === "skill_telemetry_record") {
1202
- if (!storageBackend?.supportsPostgresFeatures) {
1203
- sendResponse(socket, {
1204
- id,
1205
- ok: false,
1206
- error: "Skill telemetry requires Postgres backend"
1207
- });
1208
- socket.end();
1209
- return;
1210
- }
1211
- try {
1212
- const p = params;
1213
- let project_slug = null;
1214
- if (p.cwd) {
1215
- const row = await registryBackend.findProjectByCwdPrefix(p.cwd);
1216
- if (row) project_slug = row.slug;
1217
- }
1218
- await storageBackend.recordSkillInvocation({
1219
- skill_name: p.skill_name,
1220
- source: p.source,
1221
- scope: p.scope,
1222
- project_slug
1223
- });
1224
- sendResponse(socket, {
1225
- id,
1226
- ok: true,
1227
- result: { ok: true }
1228
- });
1229
- } catch (e) {
1230
- sendResponse(socket, {
1231
- id,
1232
- ok: false,
1233
- error: e instanceof Error ? e.message : String(e)
1234
- });
1235
- }
1236
- socket.end();
1237
- return;
1238
- }
1239
- if (method === "skill_telemetry_query") {
1240
- if (!storageBackend?.supportsPostgresFeatures) {
1241
- sendResponse(socket, {
1242
- id,
1243
- ok: false,
1244
- error: "Skill telemetry requires Postgres backend"
1245
- });
1246
- socket.end();
1247
- return;
1248
- }
1249
- try {
1250
- const p = params;
1251
- sendResponse(socket, {
1252
- id,
1253
- ok: true,
1254
- result: await storageBackend.querySkillTelemetry({
1255
- scope: p.scope,
1256
- status: p.status,
1257
- limit: p.limit
1258
- })
1259
- });
1260
- } catch (e) {
1261
- sendResponse(socket, {
1262
- id,
1263
- ok: false,
1264
- error: e instanceof Error ? e.message : String(e)
1265
- });
1266
- }
1267
- socket.end();
1268
- return;
1269
- }
1270
- if (method === "observation_query") {
1271
- if (!storageBackend?.supportsPostgresFeatures) {
1272
- sendResponse(socket, {
1273
- id,
1274
- ok: false,
1275
- error: "Observations require Postgres backend"
1276
- });
1277
- socket.end();
1278
- return;
1279
- }
1280
- try {
1281
- const p = params;
1282
- sendResponse(socket, {
1283
- id,
1284
- ok: true,
1285
- result: await storageBackend.queryObservations({
1286
- projectId: p.project_id,
1287
- sessionId: p.session_id,
1288
- type: p.type,
1289
- limit: p.limit,
1290
- offset: p.offset
1291
- })
1292
- });
1293
- } catch (e) {
1294
- sendResponse(socket, {
1295
- id,
1296
- ok: false,
1297
- error: e instanceof Error ? e.message : String(e)
1298
- });
1299
- }
1300
- socket.end();
1301
- return;
1302
- }
1303
- if (method === "observation_recent") {
1304
- if (!storageBackend?.supportsPostgresFeatures) {
1305
- sendResponse(socket, {
1306
- id,
1307
- ok: false,
1308
- error: "Observations require Postgres backend"
1309
- });
1310
- socket.end();
1311
- return;
1312
- }
1313
- try {
1314
- const p = params;
1315
- const limit = p.limit ?? 20;
1316
- let resolvedProjectId = p.project_id;
1317
- let resolvedProjectSlug;
1318
- if (resolvedProjectId === void 0 && p.cwd) {
1319
- const row = await registryBackend.findProjectByCwdPrefix(p.cwd);
1320
- if (row) {
1321
- resolvedProjectId = row.id;
1322
- resolvedProjectSlug = row.slug;
1323
- }
1324
- }
1325
- let rows;
1326
- if (resolvedProjectId !== void 0) rows = await storageBackend.queryRecentObservations(resolvedProjectId, limit);
1327
- else rows = await storageBackend.queryObservations({ limit });
1328
- sendResponse(socket, {
1329
- id,
1330
- ok: true,
1331
- result: {
1332
- rows,
1333
- project_slug: resolvedProjectSlug
1334
- }
1335
- });
1336
- } catch (e) {
1337
- sendResponse(socket, {
1338
- id,
1339
- ok: false,
1340
- error: e instanceof Error ? e.message : String(e)
1341
- });
1342
- }
1343
- socket.end();
1344
- return;
1345
- }
1346
- if (method === "observation_list") {
1347
- if (!storageBackend?.supportsPostgresFeatures) {
1348
- sendResponse(socket, {
1349
- id,
1350
- ok: false,
1351
- error: "Observations require Postgres backend"
1352
- });
1353
- socket.end();
1354
- return;
1355
- }
1356
- try {
1357
- const p = params;
1358
- let projectId;
1359
- if (p.project_slug) projectId = (await registryBackend.getProjectBySlug(p.project_slug))?.id;
1360
- sendResponse(socket, {
1361
- id,
1362
- ok: true,
1363
- result: await storageBackend.queryObservations({
1364
- projectId,
1365
- sessionId: p.session_id,
1366
- type: p.type,
1367
- limit: p.limit ?? 20,
1368
- offset: p.offset ?? 0
1369
- })
1370
- });
1371
- } catch (e) {
1372
- sendResponse(socket, {
1373
- id,
1374
- ok: false,
1375
- error: e instanceof Error ? e.message : String(e)
1376
- });
1377
- }
1378
- socket.end();
1379
- return;
1380
- }
1381
- if (method === "observation_stats") {
1382
- if (!storageBackend?.supportsPostgresFeatures) {
1383
- sendResponse(socket, {
1384
- id,
1385
- ok: false,
1386
- error: "Observations require Postgres backend"
1387
- });
1388
- socket.end();
1389
- return;
1390
- }
1391
- try {
1392
- sendResponse(socket, {
1393
- id,
1394
- ok: true,
1395
- result: await storageBackend.getObservationStats()
1396
- });
1397
- } catch (e) {
1398
- sendResponse(socket, {
1399
- id,
1400
- ok: false,
1401
- error: e instanceof Error ? e.message : String(e)
1402
- });
1403
- }
1404
- socket.end();
1405
- return;
1406
- }
1407
- if (method === "session_summary_store") {
1408
- if (!storageBackend?.supportsPostgresFeatures) {
1409
- sendResponse(socket, {
1410
- id,
1411
- ok: false,
1412
- error: "Session summaries require Postgres backend"
1413
- });
1414
- socket.end();
1415
- return;
1416
- }
1417
- try {
1418
- const p = params;
1419
- let resolvedProjectId = p.project_id ?? null;
1420
- let resolvedProjectSlug = p.project_slug ?? null;
1421
- if (resolvedProjectId === null && p.cwd) {
1422
- const row = await registryBackend.findProjectByCwdPrefix(p.cwd);
1423
- if (row) {
1424
- resolvedProjectId = row.id;
1425
- resolvedProjectSlug = row.slug;
1426
- }
1427
- }
1428
- await storageBackend.storeSessionSummary({
1429
- session_id: p.session_id,
1430
- project_id: resolvedProjectId,
1431
- project_slug: resolvedProjectSlug,
1432
- request: p.request ?? null,
1433
- investigated: p.investigated ?? null,
1434
- learned: p.learned ?? null,
1435
- completed: p.completed ?? null,
1436
- next_steps: p.next_steps ?? null,
1437
- observation_count: p.observation_count ?? 0
1438
- });
1439
- sendResponse(socket, {
1440
- id,
1441
- ok: true,
1442
- result: { ok: true }
1443
- });
1444
- } catch (e) {
1445
- sendResponse(socket, {
1446
- id,
1447
- ok: false,
1448
- error: e instanceof Error ? e.message : String(e)
1449
- });
1450
- }
1451
- socket.end();
1452
- return;
1453
- }
1454
- if (method === "work_queue_enqueue") {
1455
- try {
1456
- const p = params;
1457
- if (!p.type) {
1458
- sendResponse(socket, {
1459
- id,
1460
- ok: false,
1461
- error: "work_queue_enqueue: type is required"
1462
- });
1463
- socket.end();
1464
- return;
1465
- }
1466
- const item = enqueue({
1467
- type: p.type,
1468
- priority: p.priority,
1469
- payload: p.payload ?? {},
1470
- maxAttempts: p.maxAttempts
1471
- });
1472
- /* @__PURE__ */ notifyNewWork();
1473
- sendResponse(socket, {
1474
- id,
1475
- ok: true,
1476
- result: {
1477
- id: item.id,
1478
- status: item.status
1479
- }
1480
- });
1481
- } catch (e) {
1482
- sendResponse(socket, {
1483
- id,
1484
- ok: false,
1485
- error: e instanceof Error ? e.message : String(e)
1486
- });
1487
- }
1488
- socket.end();
1489
- return;
1490
- }
1491
- if (method === "work_queue_stats") {
1492
- sendResponse(socket, {
1493
- id,
1494
- ok: true,
1495
- result: getStats()
1496
- });
1497
- socket.end();
1498
- return;
1499
- }
1500
- try {
1501
- sendResponse(socket, {
1502
- id,
1503
- ok: true,
1504
- result: await dispatchTool(method, params)
1505
- });
1506
- } catch (e) {
1507
- sendResponse(socket, {
1508
- id,
1509
- ok: false,
1510
- error: e instanceof Error ? e.message : String(e)
1511
- });
1512
- }
1513
- socket.end();
1514
- }
1515
-
1516
- //#endregion
1517
- //#region src/storage/postgres/fts-migration.ts
1518
- const DONE_MARKER = "fts:simple";
1519
- const PROGRESS_PREFIX = "fts:simple:inprogress:";
1520
- const BATCH_SIZE = 1e3;
1521
- const MAX_ATTEMPTS = 5;
1522
- const RETRYABLE_CODES = new Set(["40P01", "40001"]);
1523
- function sleep(ms) {
1524
- return new Promise((resolve) => setTimeout(resolve, ms));
1525
- }
1526
- async function migrateFtsConfig(backend) {
1527
- const pool = backend.getPool();
1528
- if ((await pool.query(`SELECT 1 FROM information_schema.tables WHERE table_name = 'pai_chunks'`)).rowCount === 0) return;
1529
- const comment = (await pool.query(`SELECT col_description('pai_chunks'::regclass, ordinal_position) AS comment
1530
- FROM information_schema.columns
1531
- WHERE table_name = 'pai_chunks' AND column_name = 'fts_vector'`)).rows[0]?.comment ?? null;
1532
- if (comment === DONE_MARKER) return;
1533
- const resumeId = comment?.startsWith(PROGRESS_PREFIX) ? comment.slice(22) : "";
1534
- process.stderr.write("[pai-postgres] FTS config migration: starting (english -> simple)\n");
1535
- if ((await pool.query(`SELECT 1 FROM pg_proc WHERE proname = 'pai_chunks_fts_update'`)).rowCount !== 0) {
1536
- await pool.query(`
1537
- CREATE OR REPLACE FUNCTION pai_chunks_fts_update()
1538
- RETURNS TRIGGER AS $$
1539
- BEGIN
1540
- NEW.fts_vector := to_tsvector('simple', COALESCE(NEW.text, ''));
1541
- RETURN NEW;
1542
- END;
1543
- $$ LANGUAGE plpgsql;
1544
- `);
1545
- process.stderr.write("[pai-postgres] FTS config migration: trigger function now uses 'simple'\n");
1546
- }
1547
- let lastId = resumeId || null;
1548
- if (lastId !== null) process.stderr.write(`[pai-postgres] FTS config migration: resuming from ${lastId}\n`);
1549
- let totalUpdated = 0;
1550
- let batchN = 0;
1551
- while (true) {
1552
- const sql = lastId === null ? `SELECT id FROM pai_chunks ORDER BY id LIMIT $1` : `SELECT id FROM pai_chunks WHERE id > $2 ORDER BY id LIMIT $1`;
1553
- const params = lastId === null ? [BATCH_SIZE] : [BATCH_SIZE, lastId];
1554
- const idRows = await pool.query(sql, params);
1555
- if (idRows.rows.length === 0) break;
1556
- const ids = idRows.rows.map((r) => r.id);
1557
- for (let attempt = 1;; attempt++) try {
1558
- const result = await pool.query(`UPDATE pai_chunks c SET fts_vector = to_tsvector('simple', COALESCE(c.text, ''))
1559
- FROM (SELECT id FROM pai_chunks WHERE id = ANY($1::text[]) ORDER BY id FOR UPDATE) s
1560
- WHERE c.id = s.id
1561
- AND c.fts_vector IS DISTINCT FROM to_tsvector('simple', COALESCE(c.text, ''))`, [ids]);
1562
- totalUpdated += result.rowCount ?? 0;
1563
- break;
1564
- } catch (err) {
1565
- const code = err.code;
1566
- if (attempt >= MAX_ATTEMPTS || !code || !RETRYABLE_CODES.has(code)) throw err;
1567
- await sleep(200 * attempt);
1568
- }
1569
- lastId = ids[ids.length - 1];
1570
- batchN++;
1571
- if (batchN % 20 === 0) {
1572
- await pool.query(`COMMENT ON COLUMN pai_chunks.fts_vector IS '${PROGRESS_PREFIX}${lastId.replace(/'/g, "''")}'`);
1573
- process.stderr.write(`[pai-postgres] FTS config migration: ${totalUpdated} chunks rebuilt so far...\n`);
1574
- }
1575
- if (idRows.rows.length < BATCH_SIZE) break;
1576
- }
1577
- await pool.query(`COMMENT ON COLUMN pai_chunks.fts_vector IS '${DONE_MARKER}'`);
1578
- process.stderr.write(`[pai-postgres] FTS config migration: done, ${totalUpdated} chunks rebuilt\n`);
1579
- }
1580
-
1581
- //#endregion
1582
- //#region src/daemon/daemon/server.ts
1583
- /**
1584
- * IPC server and daemon entry point.
1585
- * Owns: isSocketLive, startIpcServer, serve (exported).
1586
- */
1587
- /**
1588
- * Check whether an existing socket file is actually being served by a live process.
1589
- */
1590
- function isSocketLive(path) {
1591
- return new Promise((resolve) => {
1592
- const client = connect(path);
1593
- const timer = setTimeout(() => {
1594
- client.destroy();
1595
- resolve(false);
1596
- }, 500);
1597
- client.on("connect", () => {
1598
- clearTimeout(timer);
1599
- client.end();
1600
- resolve(true);
1601
- });
1602
- client.on("error", () => {
1603
- clearTimeout(timer);
1604
- resolve(false);
1605
- });
1606
- });
1607
- }
1608
- /**
1609
- * Start the Unix Domain Socket IPC server.
1610
- */
1611
- async function startIpcServer(socketPath) {
1612
- if (existsSync(socketPath)) {
1613
- if (await isSocketLive(socketPath)) throw new Error("Another daemon is already running — socket is live. Aborting startup.");
1614
- try {
1615
- unlinkSync(socketPath);
1616
- process.stderr.write("[pai-daemon] Removed stale socket file.\n");
1617
- } catch {}
1618
- }
1619
- const server = createServer((socket) => {
1620
- let buffer = "";
1621
- socket.on("data", (chunk) => {
1622
- buffer += chunk.toString();
1623
- let nl;
1624
- while ((nl = buffer.indexOf("\n")) !== -1) {
1625
- const line = buffer.slice(0, nl);
1626
- buffer = buffer.slice(nl + 1);
1627
- if (line.trim() === "") continue;
1628
- let request;
1629
- try {
1630
- request = JSON.parse(line);
1631
- } catch {
1632
- sendResponse(socket, {
1633
- id: "?",
1634
- ok: false,
1635
- error: "Invalid JSON"
1636
- });
1637
- socket.destroy();
1638
- return;
1639
- }
1640
- handleRequest(request, socket).catch((e) => {
1641
- const msg = e instanceof Error ? e.message : String(e);
1642
- sendResponse(socket, {
1643
- id: request.id,
1644
- ok: false,
1645
- error: msg
1646
- });
1647
- socket.destroy();
1648
- });
1649
- }
1650
- });
1651
- socket.on("error", () => {});
1652
- });
1653
- server.on("error", (e) => {
1654
- process.stderr.write(`[pai-daemon] IPC server error: ${e}\n`);
1655
- });
1656
- server.listen(socketPath, () => {
1657
- process.stderr.write(`[pai-daemon] IPC server listening on ${socketPath}\n`);
1658
- });
1659
- return server;
1660
- }
1661
- async function serve(config) {
1662
- setDaemonConfig(config);
1663
- setStartTime(Date.now());
1664
- setNotificationConfig(loadNotificationConfig());
1665
- process.stderr.write("[pai-daemon] Starting daemon...\n");
1666
- process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\n`);
1667
- process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\n`);
1668
- const { notificationConfig } = await import("./state-DW8zdweW.mjs");
1669
- process.stderr.write(`[pai-daemon] Notification mode: ${notificationConfig.mode}\n`);
1670
- try {
1671
- setPriority(process.pid, 10);
1672
- } catch {}
1673
- configureEmbeddingModel(config.embeddingModel);
1674
- createRegistryBackend(config, { waitForPostgres: true }).then((backend) => {
1675
- setRegistryBackend(backend);
1676
- process.stderr.write(`[pai-daemon] Registry backend: ${backend.backendType}\n`);
1677
- }).catch((e) => {
1678
- const msg = e instanceof Error ? e.message : String(e);
1679
- process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\n`);
1680
- process.exit(1);
1681
- });
1682
- const server = await startIpcServer(config.socketPath);
1683
- startRegistryScanScheduler();
1684
- startWorkerSupervisor();
1685
- startCacheKeepalive();
1686
- startSessionKeepalive();
1687
- createStorageBackend(config, { waitForPostgres: true }).then((backend) => {
1688
- setStorageBackend(backend);
1689
- process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
1690
- startIndexScheduler();
1691
- if (backend.supportsPostgresFeatures) {
1692
- startEmbedScheduler();
1693
- migrateFtsConfig(backend).catch((e) => {
1694
- const msg = e instanceof Error ? e.message : String(e);
1695
- process.stderr.write(`[pai-daemon] FTS config migration failed: ${msg}\n`);
1696
- });
1697
- } else process.stderr.write("[pai-daemon] Embed scheduler: disabled (SQLite backend)\n");
1698
- loadQueue();
1699
- startWorker();
1700
- }).catch((e) => {
1701
- const msg = e instanceof Error ? e.message : String(e);
1702
- process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
1703
- process.exit(1);
1704
- });
1705
- const shutdown = async (signal) => {
1706
- process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
1707
- setShutdownRequested(true);
1708
- if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);
1709
- if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
1710
- if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);
1711
- stopWorker();
1712
- server.close();
1713
- const SHUTDOWN_TIMEOUT_MS = 1e4;
1714
- const POLL_INTERVAL_MS = 100;
1715
- const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;
1716
- if (indexInProgress || embedInProgress) {
1717
- process.stderr.write(`[pai-daemon] Waiting for in-progress operations to finish (index=${indexInProgress}, embed=${embedInProgress})...\n`);
1718
- while ((indexInProgress || embedInProgress) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1719
- if (indexInProgress || embedInProgress) process.stderr.write("[pai-daemon] Shutdown timeout reached — forcing exit.\n");
1720
- else process.stderr.write("[pai-daemon] In-progress operations finished.\n");
1721
- }
1722
- try {
1723
- await storageBackend.close();
1724
- } catch {}
1725
- try {
1726
- unlinkSync(config.socketPath);
1727
- } catch {}
1728
- process.exit(0);
1729
- };
1730
- process.on("SIGINT", () => {
1731
- shutdown("SIGINT").catch(() => process.exit(0));
1732
- });
1733
- process.on("SIGTERM", () => {
1734
- shutdown("SIGTERM").catch(() => process.exit(0));
1735
- });
1736
- await new Promise(() => {});
1737
- }
1738
-
1739
- //#endregion
1740
- export { serve as t };
1741
- //# sourceMappingURL=daemon-DXlaVCkw.mjs.map