harnery 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,450 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
4
+ import { hostname } from "node:os";
5
+ import { resolve } from "node:path";
6
+ import { performance } from "node:perf_hooks";
7
+ import { collectCoordinationHealthSnapshot } from "../agents/health.js";
8
+ import { checkPidToken, processStartToken } from "../agents/state/proc-start.js";
9
+ import { sampleResources } from "../resources/sampler.js";
10
+ import { requestResourceServiceStop } from "../resources/service.js";
11
+ import { readResourceServiceStatus } from "../resources/service-status.js";
12
+ import { resourcePaths } from "../resources/storage.js";
13
+ import { writePrivateJsonAtomic } from "../storage/atomic-json.js";
14
+ import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.js";
15
+ import { collectSupervisorActivitySnapshot } from "./activity.js";
16
+ import { SUPERVISOR_SNAPSHOT_SCHEMA_VERSION, SUPERVISOR_STATUS_SCHEMA_VERSION, } from "./contract.js";
17
+ import { explainSupervisorFinding } from "./explanations.js";
18
+ import { updateSupervisorFindings } from "./findings.js";
19
+ import { updateSupervisorHistory } from "./history.js";
20
+ import { projectHookHealth } from "./hook-health.js";
21
+ import { writeSupervisorHookHealth } from "./hook-health-storage.js";
22
+ import { collectHookHealth } from "./hooks.js";
23
+ import { SupervisorLogCollector } from "./log-feed.js";
24
+ import { collectServiceHealth } from "./services.js";
25
+ import { readSupervisorStatus } from "./status.js";
26
+ import { readSupervisorFindings, readSupervisorHistory, supervisorPaths } from "./storage.js";
27
+ import { buildSupervisorTimeline } from "./timeline.js";
28
+ export const SUPERVISOR_DEFAULT_INTERVAL_MS = 2_000;
29
+ export const SUPERVISOR_DEFAULT_IDLE_EXIT_MS = 2 * 60_000;
30
+ const SUPERVISOR_MIN_INTERVAL_MS = 500;
31
+ const SUPERVISOR_MAX_INTERVAL_MS = 60_000;
32
+ const SUPERVISOR_MIN_IDLE_EXIT_MS = 5_000;
33
+ const SUPERVISOR_MAX_IDLE_EXIT_MS = 24 * 60 * 60_000;
34
+ export async function spawnSupervisor(coordRootRaw, options = {}) {
35
+ const coordRoot = resolve(coordRootRaw);
36
+ const current = readSupervisorStatus(coordRoot);
37
+ if (current.running) {
38
+ throw new Error(`local supervisor is already running under pid ${current.record?.pid}`);
39
+ }
40
+ await stopLegacyResourceObserver(coordRoot);
41
+ const intervalMs = normalizeInterval(options.intervalMs);
42
+ const idleExitMs = normalizeIdleExit(options.idleExitMs);
43
+ const paths = supervisorPaths(coordRoot);
44
+ mkdirSync(paths.root, { recursive: true, mode: 0o700 });
45
+ rmSync(paths.stop, { force: true });
46
+ const harnBin = new URL("../../../bin/harn", import.meta.url).pathname;
47
+ if (!existsSync(harnBin))
48
+ throw new Error(`cannot find harn executable at ${harnBin}`);
49
+ let spawnError;
50
+ const child = spawn(harnBin, [
51
+ "supervisor",
52
+ "daemon",
53
+ "--root",
54
+ coordRoot,
55
+ "--interval-ms",
56
+ String(intervalMs),
57
+ "--idle-exit-ms",
58
+ String(idleExitMs),
59
+ ...(options.keepAlive ? ["--keep-alive"] : []),
60
+ ], {
61
+ cwd: coordRoot,
62
+ detached: true,
63
+ stdio: ["ignore", "ignore", "ignore"],
64
+ env: {
65
+ ...process.env,
66
+ HARNERY_COORD_ROOT_OVERRIDE: coordRoot,
67
+ HARNERY_OUTPUT_SESSION_TEE: "0",
68
+ },
69
+ });
70
+ child.once("error", (error) => {
71
+ spawnError = error;
72
+ });
73
+ child.unref();
74
+ const deadline = Date.now() + 5_000;
75
+ while (Date.now() < deadline) {
76
+ await delay(50);
77
+ if (spawnError)
78
+ throw spawnError;
79
+ const status = readSupervisorStatus(coordRoot);
80
+ if (status.running && status.record?.pid === child.pid)
81
+ return status;
82
+ if (child.exitCode !== null)
83
+ break;
84
+ }
85
+ throw new Error(`local supervisor failed to start; inspect ${paths.service}`);
86
+ }
87
+ export async function ensureSupervisorRunning(coordRoot) {
88
+ const status = readSupervisorStatus(coordRoot);
89
+ if (status.running)
90
+ return { state: "running", status };
91
+ try {
92
+ const started = await spawnSupervisor(coordRoot);
93
+ return { state: "started", status: started };
94
+ }
95
+ catch (error) {
96
+ return {
97
+ state: "unavailable",
98
+ status: readSupervisorStatus(coordRoot),
99
+ error: error instanceof Error ? error.message : String(error),
100
+ };
101
+ }
102
+ }
103
+ export function requestSupervisorStop(coordRootRaw) {
104
+ const coordRoot = resolve(coordRootRaw);
105
+ const paths = supervisorPaths(coordRoot);
106
+ const status = readSupervisorStatus(coordRoot);
107
+ mkdirSync(paths.root, { recursive: true, mode: 0o700 });
108
+ writePrivateJsonAtomic(paths.stop, {
109
+ requested_at: new Date().toISOString(),
110
+ requested_by_pid: process.pid,
111
+ });
112
+ if (status.running && status.record?.host === hostname()) {
113
+ try {
114
+ process.kill(status.record.pid, "SIGTERM");
115
+ }
116
+ catch {
117
+ // The durable stop file handles a racing exit.
118
+ }
119
+ }
120
+ return readSupervisorStatus(coordRoot);
121
+ }
122
+ export async function runSupervisor(input) {
123
+ const coordRoot = resolve(input.coordRoot);
124
+ const paths = supervisorPaths(coordRoot);
125
+ const intervalMs = normalizeInterval(input.intervalMs);
126
+ const idleExitMs = normalizeIdleExit(input.idleExitMs);
127
+ const keepAlive = input.keepAlive ?? false;
128
+ const now = input.now ?? (() => new Date());
129
+ const wait = input.wait ?? delay;
130
+ const lease = acquireLease(coordRoot, now());
131
+ rmSync(paths.stop, { force: true });
132
+ const startedAt = now().toISOString();
133
+ const startToken = processStartToken(process.pid);
134
+ const status = {
135
+ schema_version: SUPERVISOR_STATUS_SCHEMA_VERSION,
136
+ pid: process.pid,
137
+ ...(startToken ? { start_token: startToken } : {}),
138
+ host: hostname(),
139
+ nonce: lease.nonce,
140
+ state: "starting",
141
+ started_at: startedAt,
142
+ heartbeat_at: startedAt,
143
+ interval_ms: intervalMs,
144
+ keep_alive: keepAlive,
145
+ idle_exit_ms: idleExitMs,
146
+ cycle_count: 0,
147
+ };
148
+ let stopping = false;
149
+ let previousResource;
150
+ let history = readSupervisorHistory(coordRoot);
151
+ let findings = readSupervisorFindings(coordRoot);
152
+ let coordination = collectCoordinationHealthSnapshot(coordRoot, now());
153
+ const logs = new SupervisorLogCollector(coordRoot);
154
+ const writeStatus = () => {
155
+ status.heartbeat_at = now().toISOString();
156
+ writePrivateJsonAtomic(paths.service, status);
157
+ };
158
+ const requestStop = () => {
159
+ stopping = true;
160
+ status.state = "stopping";
161
+ writeStatus();
162
+ };
163
+ process.on("SIGINT", requestStop);
164
+ process.on("SIGTERM", requestStop);
165
+ status.state = "running";
166
+ writeStatus();
167
+ log(coordRoot, "supervisor.started", {
168
+ pid: process.pid,
169
+ interval_ms: intervalMs,
170
+ idle_exit_ms: idleExitMs,
171
+ keep_alive: keepAlive,
172
+ });
173
+ try {
174
+ while (!stopping && !existsSync(paths.stop)) {
175
+ const cycleStarted = performance.now();
176
+ try {
177
+ const cycleNow = now();
178
+ const serviceHealth = collectServiceHealth(coordRoot, status);
179
+ const resource = sampleResources(coordRoot, previousResource, {
180
+ services: [
181
+ { pid: process.pid, id: "supervisor" },
182
+ ...serviceHealth.consumers.map((consumer) => ({
183
+ pid: consumer.pid,
184
+ id: consumer.id,
185
+ })),
186
+ ],
187
+ });
188
+ previousResource = resource.state;
189
+ writePrivateJsonAtomic(resourcePaths(coordRoot).snapshot, resource.snapshot);
190
+ const logFeed = await logs.collect(cycleNow);
191
+ writePrivateJsonAtomic(paths.log_feed, logFeed);
192
+ const hookHealth = projectHookHealth(logFeed, cycleNow);
193
+ writeSupervisorHookHealth(coordRoot, hookHealth);
194
+ const activity = collectSupervisorActivitySnapshot(coordRoot, cycleNow);
195
+ writePrivateJsonAtomic(paths.activity, activity);
196
+ const hooks = collectHookHealth(resource.snapshot);
197
+ const historyResult = updateSupervisorHistory(history, resource.snapshot);
198
+ history = historyResult.history;
199
+ if (historyResult.changed)
200
+ writePrivateJsonAtomic(paths.history, history);
201
+ if (historyResult.changed)
202
+ coordination = collectCoordinationHealthSnapshot(coordRoot, cycleNow);
203
+ writePrivateJsonAtomic(paths.coordination_health, coordination);
204
+ const priorActiveFindingIds = new Set(findings?.active.map((finding) => finding.id) ?? []);
205
+ const priorTransitionKeys = new Set(findings?.transitions.map((finding) => `${finding.id}:${finding.state}`) ?? []);
206
+ findings = updateSupervisorFindings({
207
+ previous: findings,
208
+ resource: resource.snapshot,
209
+ services: serviceHealth.services,
210
+ hooks,
211
+ history,
212
+ logFeed,
213
+ hookHealth,
214
+ coordination,
215
+ activity,
216
+ now: cycleNow,
217
+ });
218
+ writePrivateJsonAtomic(paths.findings, findings);
219
+ mkdirSync(paths.timelines, { recursive: true, mode: 0o700 });
220
+ mkdirSync(paths.explanations, { recursive: true, mode: 0o700 });
221
+ const projectionUpdates = [
222
+ ...findings.active.filter((finding) => !priorActiveFindingIds.has(finding.id)),
223
+ ...findings.transitions.filter((finding) => !priorTransitionKeys.has(`${finding.id}:${finding.state}`)),
224
+ ];
225
+ for (const finding of projectionUpdates) {
226
+ const relatedSources = coordination.recent_events.filter((source) => {
227
+ const delta = Math.abs(Date.parse(source.observed_at) - Date.parse(finding.observed_at));
228
+ return Number.isFinite(delta) && delta <= 5 * 60_000;
229
+ });
230
+ writePrivateJsonAtomic(resolve(paths.timelines, `${finding.id}.json`), buildSupervisorTimeline(finding, relatedSources));
231
+ writePrivateJsonAtomic(resolve(paths.explanations, `${finding.id}.json`), explainSupervisorFinding(finding));
232
+ }
233
+ const retainedFindingIds = new Set([
234
+ ...findings.active.map((finding) => finding.id),
235
+ ...findings.transitions.map((finding) => finding.id),
236
+ ]);
237
+ pruneProjectionDirectory(paths.timelines, retainedFindingIds);
238
+ pruneProjectionDirectory(paths.explanations, retainedFindingIds);
239
+ const attributedAgentCount = resource.snapshot.groups.filter((group) => group.kind === "agent").length;
240
+ const idle = serviceHealth.consumers.length === 0 && attributedAgentCount === 0;
241
+ if (!keepAlive && idle)
242
+ status.idle_since ??= now().toISOString();
243
+ else
244
+ status.idle_since = undefined;
245
+ const snapshot = {
246
+ schema_version: SUPERVISOR_SNAPSHOT_SCHEMA_VERSION,
247
+ sampled_at: resource.snapshot.sampled_at,
248
+ sequence: status.cycle_count + 1,
249
+ collector_duration_ms: round1(performance.now() - cycleStarted),
250
+ resource_sample_duration_ms: resource.snapshot.sample_duration_ms,
251
+ services: serviceHealth.services,
252
+ hooks,
253
+ active_finding_count: findings.active.length,
254
+ history_point_count: history.points.length,
255
+ log_record_count: logFeed.total_records,
256
+ live_consumer_count: serviceHealth.consumers.length,
257
+ attributed_agent_count: attributedAgentCount,
258
+ };
259
+ writePrivateJsonAtomic(paths.snapshot, snapshot);
260
+ status.cycle_count += 1;
261
+ status.last_cycle_at = snapshot.sampled_at;
262
+ status.last_cycle_duration_ms = snapshot.collector_duration_ms;
263
+ status.last_error_code = undefined;
264
+ log(coordRoot, "supervisor.sample", {
265
+ cycle_count: status.cycle_count,
266
+ duration_ms: snapshot.collector_duration_ms,
267
+ resource_cpu_ms: resource.snapshot.collector_cpu_ms,
268
+ visible_processes: resource.snapshot.visible_process_count,
269
+ services: serviceHealth.services.length,
270
+ hooks: hooks.length,
271
+ findings: findings.active.length,
272
+ log_records: logFeed.total_records,
273
+ hook_health_records: hookHealth.source_record_count,
274
+ });
275
+ if (!keepAlive &&
276
+ status.idle_since &&
277
+ now().getTime() - Date.parse(status.idle_since) >= idleExitMs) {
278
+ log(coordRoot, "supervisor.idle_exit", { idle_exit_ms: idleExitMs });
279
+ break;
280
+ }
281
+ }
282
+ catch (error) {
283
+ status.last_error_code = supervisorErrorCode(error);
284
+ status.state = "error";
285
+ log(coordRoot, "supervisor.sample_error", { reason_code: status.last_error_code }, error);
286
+ }
287
+ if (status.state === "error")
288
+ status.state = "running";
289
+ writeStatus();
290
+ if (input.maxCycles !== undefined && status.cycle_count >= input.maxCycles)
291
+ break;
292
+ const remaining = Math.max(0, intervalMs - (performance.now() - cycleStarted));
293
+ if (!stopping && !existsSync(paths.stop))
294
+ await wait(remaining);
295
+ }
296
+ }
297
+ finally {
298
+ process.off("SIGINT", requestStop);
299
+ process.off("SIGTERM", requestStop);
300
+ status.state = "stopped";
301
+ status.stopped_at = now().toISOString();
302
+ status.idle_since = undefined;
303
+ writeStatus();
304
+ log(coordRoot, "supervisor.stopped", { cycle_count: status.cycle_count });
305
+ try {
306
+ await closeProcessLoggers();
307
+ }
308
+ finally {
309
+ releaseLease(paths.lease, lease.nonce);
310
+ }
311
+ }
312
+ return status;
313
+ }
314
+ function acquireLease(coordRoot, now) {
315
+ const paths = supervisorPaths(coordRoot);
316
+ mkdirSync(paths.root, { recursive: true, mode: 0o700 });
317
+ for (let attempt = 0; attempt < 2; attempt++) {
318
+ const startToken = processStartToken(process.pid);
319
+ const lease = {
320
+ pid: process.pid,
321
+ ...(startToken ? { start_token: startToken } : {}),
322
+ host: hostname(),
323
+ nonce: randomUUID(),
324
+ created_at: now.toISOString(),
325
+ };
326
+ try {
327
+ const fd = openSync(paths.lease, "wx", 0o600);
328
+ try {
329
+ writeFileSync(fd, `${JSON.stringify(lease)}\n`, "utf8");
330
+ }
331
+ finally {
332
+ closeSync(fd);
333
+ }
334
+ return lease;
335
+ }
336
+ catch (error) {
337
+ if (error.code !== "EEXIST")
338
+ throw error;
339
+ const existing = readLease(paths.lease);
340
+ if (existing && leaseIsLive(existing)) {
341
+ throw new Error(`local supervisor lease is held by pid ${existing.pid}`);
342
+ }
343
+ try {
344
+ unlinkSync(paths.lease);
345
+ }
346
+ catch {
347
+ // A concurrent starter may have replaced or removed the stale lease.
348
+ }
349
+ }
350
+ }
351
+ throw new Error("local supervisor could not acquire its singleton lease");
352
+ }
353
+ function pruneProjectionDirectory(directory, retainedFindingIds) {
354
+ for (const file of readdirSync(directory)) {
355
+ if (!file.endsWith(".json"))
356
+ continue;
357
+ const findingId = file.slice(0, -5);
358
+ if (!retainedFindingIds.has(findingId))
359
+ rmSync(resolve(directory, file), { force: true });
360
+ }
361
+ }
362
+ function releaseLease(path, nonce) {
363
+ const current = readLease(path);
364
+ if (current?.nonce !== nonce)
365
+ return;
366
+ try {
367
+ unlinkSync(path);
368
+ }
369
+ catch {
370
+ // A missing lease is already released.
371
+ }
372
+ }
373
+ function readLease(path) {
374
+ try {
375
+ const value = JSON.parse(readFileSync(path, "utf8"));
376
+ return value && Number.isSafeInteger(value.pid) && typeof value.nonce === "string"
377
+ ? value
378
+ : undefined;
379
+ }
380
+ catch {
381
+ return undefined;
382
+ }
383
+ }
384
+ function leaseIsLive(lease) {
385
+ if (lease.host !== hostname())
386
+ return true;
387
+ try {
388
+ process.kill(lease.pid, 0);
389
+ }
390
+ catch (error) {
391
+ if (error.code !== "EPERM")
392
+ return false;
393
+ }
394
+ return checkPidToken(lease.pid, lease.start_token) !== "mismatch";
395
+ }
396
+ async function stopLegacyResourceObserver(coordRoot) {
397
+ const legacy = readResourceServiceStatus(coordRoot);
398
+ if (!legacy.running)
399
+ return;
400
+ requestResourceServiceStop(coordRoot);
401
+ const deadline = Date.now() + 2_000;
402
+ while (readResourceServiceStatus(coordRoot).running && Date.now() < deadline)
403
+ await delay(50);
404
+ }
405
+ function log(coordRoot, event, fields, error) {
406
+ try {
407
+ const logger = processLogger(coordRoot, "supervisor");
408
+ if (error)
409
+ logger.error(event, legacyLogFields(fields), error);
410
+ else
411
+ logger.info(event, legacyLogFields(fields));
412
+ }
413
+ catch {
414
+ // Observability must never break the supervisor loop.
415
+ }
416
+ }
417
+ function supervisorErrorCode(error) {
418
+ const message = error instanceof Error ? error.message : String(error);
419
+ if (/procfs|\/proc\//i.test(message))
420
+ return "procfs_read_failed";
421
+ if (/permission|EACCES|EPERM/i.test(message))
422
+ return "permission_denied";
423
+ if (/log/i.test(message))
424
+ return "log_collection_failed";
425
+ return "collection_failed";
426
+ }
427
+ function normalizeInterval(value) {
428
+ const interval = value ?? SUPERVISOR_DEFAULT_INTERVAL_MS;
429
+ if (!Number.isFinite(interval) ||
430
+ interval < SUPERVISOR_MIN_INTERVAL_MS ||
431
+ interval > SUPERVISOR_MAX_INTERVAL_MS) {
432
+ throw new Error(`supervisor interval must be ${SUPERVISOR_MIN_INTERVAL_MS}-${SUPERVISOR_MAX_INTERVAL_MS} ms`);
433
+ }
434
+ return Math.round(interval);
435
+ }
436
+ function normalizeIdleExit(value) {
437
+ const idle = value ?? SUPERVISOR_DEFAULT_IDLE_EXIT_MS;
438
+ if (!Number.isFinite(idle) ||
439
+ idle < SUPERVISOR_MIN_IDLE_EXIT_MS ||
440
+ idle > SUPERVISOR_MAX_IDLE_EXIT_MS) {
441
+ throw new Error(`supervisor idle exit must be ${SUPERVISOR_MIN_IDLE_EXIT_MS}-${SUPERVISOR_MAX_IDLE_EXIT_MS} ms`);
442
+ }
443
+ return Math.round(idle);
444
+ }
445
+ function delay(milliseconds) {
446
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
447
+ }
448
+ function round1(value) {
449
+ return Math.round(value * 10) / 10;
450
+ }
@@ -0,0 +1,11 @@
1
+ import { type ObservedServiceHealth, type SupervisorConsumerRecord, type SupervisorServiceStatusRecord } from "./contract.js";
2
+ export declare function registerSupervisorConsumer(coordRoot: string, input: {
3
+ id: string;
4
+ pid?: number;
5
+ }): SupervisorConsumerRecord;
6
+ export declare function unregisterSupervisorConsumer(coordRoot: string, id: string): void;
7
+ export declare function collectServiceHealth(coordRoot: string, supervisor: SupervisorServiceStatusRecord): {
8
+ services: ObservedServiceHealth[];
9
+ consumers: SupervisorConsumerRecord[];
10
+ };
11
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/services.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,qBAAqB,EAE1B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EACnC,MAAM,eAAe,CAAC;AAGvB,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,wBAAwB,CAY1B;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,6BAA6B,GACxC;IAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAAC,SAAS,EAAE,wBAAwB,EAAE,CAAA;CAAE,CAuC9E"}
@@ -0,0 +1,155 @@
1
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
2
+ import { hostname } from "node:os";
3
+ import { join } from "node:path";
4
+ import { checkPidToken, processStartToken } from "../agents/state/proc-start.js";
5
+ import { writePrivateJsonAtomic } from "../storage/atomic-json.js";
6
+ import { SUPERVISOR_CONSUMER_SCHEMA_VERSION, } from "./contract.js";
7
+ import { readSupervisorConsumer, supervisorConsumerPath, supervisorPaths } from "./storage.js";
8
+ export function registerSupervisorConsumer(coordRoot, input) {
9
+ const pid = input.pid ?? process.pid;
10
+ const startToken = processStartToken(pid);
11
+ const record = {
12
+ schema_version: SUPERVISOR_CONSUMER_SCHEMA_VERSION,
13
+ id: input.id,
14
+ pid,
15
+ ...(startToken ? { start_token: startToken } : {}),
16
+ registered_at: new Date().toISOString(),
17
+ };
18
+ writePrivateJsonAtomic(supervisorConsumerPath(coordRoot, input.id), record);
19
+ return record;
20
+ }
21
+ export function unregisterSupervisorConsumer(coordRoot, id) {
22
+ rmSync(supervisorConsumerPath(coordRoot, id), { force: true });
23
+ }
24
+ export function collectServiceHealth(coordRoot, supervisor) {
25
+ const consumers = readLiveConsumers(coordRoot);
26
+ const semantic = readDetachedService(join(coordRoot, ".harnery", "semantic", "v2", "service.json"), "semantic-reader");
27
+ const governor = readDetachedService(join(coordRoot, ".harnery", "governor-service", "service.json"), "governor");
28
+ const relay = readPresenceRelay(join(coordRoot, ".harnery", "presence", "relay-daemon.json"));
29
+ const dashboard = consumers.find((consumer) => consumer.id === "dashboard");
30
+ const services = [
31
+ {
32
+ id: "supervisor",
33
+ state: supervisor.state === "running"
34
+ ? "running"
35
+ : supervisor.state === "error"
36
+ ? "stale"
37
+ : "stopped",
38
+ pid: supervisor.pid,
39
+ started_at: supervisor.started_at,
40
+ heartbeat_at: supervisor.heartbeat_at,
41
+ ...(supervisor.state === "error" ? { reason: "supervisor-reported-error" } : {}),
42
+ },
43
+ semantic,
44
+ governor,
45
+ relay,
46
+ dashboard
47
+ ? {
48
+ id: "dashboard",
49
+ state: "running",
50
+ pid: dashboard.pid,
51
+ started_at: dashboard.registered_at,
52
+ }
53
+ : { id: "dashboard", state: "stopped" },
54
+ ];
55
+ return { services, consumers };
56
+ }
57
+ function readLiveConsumers(coordRoot) {
58
+ const directory = supervisorPaths(coordRoot).consumers;
59
+ if (!existsSync(directory))
60
+ return [];
61
+ const live = [];
62
+ for (const entry of readdirSync(directory)) {
63
+ if (!entry.endsWith(".json"))
64
+ continue;
65
+ const path = `${directory}/${entry}`;
66
+ const record = readSupervisorConsumer(path);
67
+ if (!record || !consumerAlive(record)) {
68
+ rmSync(path, { force: true });
69
+ continue;
70
+ }
71
+ live.push(record);
72
+ }
73
+ return live.sort((left, right) => left.id.localeCompare(right.id));
74
+ }
75
+ function consumerAlive(record) {
76
+ if (record.schema_version !== SUPERVISOR_CONSUMER_SCHEMA_VERSION ||
77
+ !Number.isSafeInteger(record.pid) ||
78
+ record.pid < 1 ||
79
+ typeof record.id !== "string") {
80
+ return false;
81
+ }
82
+ try {
83
+ process.kill(record.pid, 0);
84
+ }
85
+ catch (error) {
86
+ if (error.code !== "EPERM")
87
+ return false;
88
+ }
89
+ return checkPidToken(record.pid, record.start_token) !== "mismatch";
90
+ }
91
+ function readDetachedService(path, id) {
92
+ const record = readBoundedJson(path);
93
+ if (!record || !Number.isSafeInteger(record.pid) || record.pid < 1) {
94
+ return { id, state: "stopped" };
95
+ }
96
+ if (record.state === "stopping" || record.state === "stopped") {
97
+ return projection(id, "stopped", record);
98
+ }
99
+ if (record.host && record.host !== hostname()) {
100
+ const age = record.heartbeat_at
101
+ ? Date.now() - Date.parse(record.heartbeat_at)
102
+ : Number.POSITIVE_INFINITY;
103
+ const state = Number.isFinite(age) && age < 120_000 ? "running" : "stale";
104
+ return projection(id, state, record, state === "stale" ? "remote-heartbeat-expired" : undefined);
105
+ }
106
+ const state = pidAlive(record.pid) ? "running" : "stale";
107
+ return projection(id, state, record, state === "stale" ? "recorded-running-process-missing" : undefined);
108
+ }
109
+ function readPresenceRelay(path) {
110
+ const record = readBoundedJson(path);
111
+ if (!record || !Number.isSafeInteger(record.pid) || record.pid < 1) {
112
+ return { id: "presence-relay", state: "stopped" };
113
+ }
114
+ const running = pidAlive(record.pid);
115
+ return {
116
+ id: "presence-relay",
117
+ state: running ? "running" : "stale",
118
+ pid: record.pid,
119
+ ...(record.started_at ? { started_at: record.started_at } : {}),
120
+ ...(!running ? { reason: "recorded-running-process-missing" } : {}),
121
+ };
122
+ }
123
+ function projection(id, state, record, reason) {
124
+ return {
125
+ id,
126
+ state,
127
+ pid: record.pid,
128
+ ...(record.started_at ? { started_at: record.started_at } : {}),
129
+ ...(record.heartbeat_at ? { heartbeat_at: record.heartbeat_at } : {}),
130
+ ...(reason ? { reason } : {}),
131
+ };
132
+ }
133
+ function readBoundedJson(path) {
134
+ if (!existsSync(path))
135
+ return undefined;
136
+ try {
137
+ const size = statSync(path).size;
138
+ if (size < 2 || size > 512 * 1_024)
139
+ return undefined;
140
+ const value = JSON.parse(readFileSync(path, "utf8"));
141
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ }
147
+ function pidAlive(pid) {
148
+ try {
149
+ process.kill(pid, 0);
150
+ return true;
151
+ }
152
+ catch (error) {
153
+ return error.code === "EPERM";
154
+ }
155
+ }
@@ -0,0 +1,3 @@
1
+ import { type SupervisorStatus } from "./contract.js";
2
+ export declare function readSupervisorStatus(coordRoot: string, nowMs?: number): SupervisorStatus;
3
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/core/supervisor/status.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAC;AAKvB,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,gBAAgB,CAyB5F"}
@@ -0,0 +1,48 @@
1
+ import { hostname } from "node:os";
2
+ import { checkPidToken } from "../agents/state/proc-start.js";
3
+ import { SUPERVISOR_STATUS_SCHEMA_VERSION, } from "./contract.js";
4
+ import { readSupervisorServiceRecord, supervisorPaths } from "./storage.js";
5
+ const STALE_HEARTBEAT_MS = 15_000;
6
+ export function readSupervisorStatus(coordRoot, nowMs = Date.now()) {
7
+ const paths = supervisorPaths(coordRoot);
8
+ const record = readSupervisorServiceRecord(coordRoot);
9
+ if (!validRecord(record)) {
10
+ return {
11
+ running: false,
12
+ stale: false,
13
+ status_path: paths.service,
14
+ snapshot_path: paths.snapshot,
15
+ };
16
+ }
17
+ const heartbeatAt = Date.parse(record.heartbeat_at);
18
+ const stale = !Number.isFinite(heartbeatAt) || nowMs - heartbeatAt > STALE_HEARTBEAT_MS;
19
+ const running = record.host === hostname() &&
20
+ !stale &&
21
+ (record.state === "starting" || record.state === "running" || record.state === "stopping") &&
22
+ pidStillMatches(record.pid, record.start_token);
23
+ return {
24
+ running,
25
+ stale,
26
+ record,
27
+ status_path: paths.service,
28
+ snapshot_path: paths.snapshot,
29
+ };
30
+ }
31
+ function validRecord(value) {
32
+ return (value?.schema_version === SUPERVISOR_STATUS_SCHEMA_VERSION &&
33
+ Number.isSafeInteger(value.pid) &&
34
+ value.pid > 0 &&
35
+ typeof value.host === "string" &&
36
+ typeof value.heartbeat_at === "string" &&
37
+ typeof value.keep_alive === "boolean");
38
+ }
39
+ function pidStillMatches(pid, startToken) {
40
+ try {
41
+ process.kill(pid, 0);
42
+ }
43
+ catch (error) {
44
+ if (error.code !== "EPERM")
45
+ return false;
46
+ }
47
+ return checkPidToken(pid, startToken) !== "mismatch";
48
+ }