harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { constants, createReadStream } from "node:fs";
2
+ import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, realpath, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { finished } from "node:stream/promises";
5
7
  import { StringDecoder } from "node:string_decoder";
6
8
  import { createGunzip, createInflateRaw } from "node:zlib";
7
9
  import { stateDirMode, stateFileMode } from "../../../storage/modes.ts";
@@ -89,24 +91,17 @@ async function* streamEventV3SupportPackRecordsForManifest(
89
91
  throw new Error("event_v3_support_payload_length_mismatch");
90
92
  if (payloadStat.size > bounded.max_payload_bytes)
91
93
  throw new Error("event_v3_support_payload_too_large");
92
- if ((await hashHandle(payloadPath, payloadHandle)) !== manifest.payload.digest) {
94
+ if ((await hashHandle(payloadHandle)) !== manifest.payload.digest) {
93
95
  throw new Error("event_v3_support_payload_digest_mismatch");
94
96
  }
95
97
  limits.fault?.("after_payload_digest", payloadPath);
96
98
  await assertCanonicalSingleGzipMember(
97
- payloadPath,
98
99
  payloadHandle,
99
100
  payloadStat.size,
100
101
  bounded.max_decompressed_bytes,
101
102
  );
102
103
 
103
104
  const gunzip = createGunzip();
104
- const source = createReadStream(payloadPath, {
105
- fd: payloadHandle.fd,
106
- autoClose: false,
107
- start: 0,
108
- });
109
- source.pipe(gunzip);
110
105
  const decoder = new StringDecoder("utf8");
111
106
  let pending = "";
112
107
  let decompressedBytes = 0;
@@ -116,7 +111,7 @@ async function* streamEventV3SupportPackRecordsForManifest(
116
111
  const entries: EventV3LogicalAuthorityEntry[] = [];
117
112
  const seen = new Set<string>();
118
113
 
119
- for await (const current of gunzip) {
114
+ for await (const current of decompressHandle(payloadHandle, 0, gunzip)) {
120
115
  const chunk = current as Buffer;
121
116
  decompressedBytes += chunk.length;
122
117
  if (decompressedBytes > bounded.max_decompressed_bytes) {
@@ -247,16 +242,49 @@ export async function unpackEventV3SupportPack(
247
242
  }
248
243
  }
249
244
 
250
- async function hashHandle(
251
- path: string,
252
- handle: Awaited<ReturnType<typeof open>>,
253
- ): Promise<`sha256:${string}`> {
245
+ async function hashHandle(handle: Awaited<ReturnType<typeof open>>): Promise<`sha256:${string}`> {
254
246
  const hash = createHash("sha256");
255
- const stream = createReadStream(path, { fd: handle.fd, autoClose: false, start: 0 });
256
- for await (const current of stream) hash.update(current as Buffer);
247
+ for await (const current of handleChunks(handle, 0)) hash.update(current);
257
248
  return `sha256:${hash.digest("hex")}`;
258
249
  }
259
250
 
251
+ async function* handleChunks(handle: Awaited<ReturnType<typeof open>>, position: number) {
252
+ while (true) {
253
+ const buffer = Buffer.alloc(64 * 1024);
254
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
255
+ if (bytesRead === 0) return;
256
+ position += bytesRead;
257
+ yield buffer.subarray(0, bytesRead);
258
+ }
259
+ }
260
+
261
+ async function* decompressHandle(
262
+ handle: Awaited<ReturnType<typeof open>>,
263
+ start: number,
264
+ decoder: ReturnType<typeof createGunzip>,
265
+ ): AsyncGenerator<Buffer> {
266
+ const source = Readable.from(handleChunks(handle, start));
267
+ // Observe failures immediately, then settle source reads before the caller
268
+ // can close and reuse the verified handle, including on early rejection.
269
+ const completed = finished(source, { cleanup: true }).then(
270
+ () => undefined,
271
+ (error: unknown) => error,
272
+ );
273
+ source.once("error", (error) => decoder.destroy(error));
274
+ source.pipe(decoder);
275
+ try {
276
+ for await (const chunk of decoder) yield chunk as Buffer;
277
+ source.unpipe(decoder);
278
+ source.resume();
279
+ const error = await completed;
280
+ if (error) throw error;
281
+ } finally {
282
+ source.destroy();
283
+ decoder.destroy();
284
+ await completed;
285
+ }
286
+ }
287
+
260
288
  async function realDirectory(path: string): Promise<string> {
261
289
  const stat = await lstat(path);
262
290
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -265,7 +293,6 @@ async function realDirectory(path: string): Promise<string> {
265
293
  }
266
294
 
267
295
  async function assertCanonicalSingleGzipMember(
268
- path: string,
269
296
  handle: Awaited<ReturnType<typeof open>>,
270
297
  payloadBytes: number,
271
298
  maximumDecompressedBytes: number,
@@ -284,9 +311,8 @@ async function assertCanonicalSingleGzipMember(
284
311
  throw new Error("event_v3_support_gzip_header_noncanonical");
285
312
  }
286
313
  const inflate = createInflateRaw();
287
- createReadStream(path, { fd: handle.fd, autoClose: false, start: 10 }).pipe(inflate);
288
314
  let decompressed = 0;
289
- for await (const current of inflate) {
315
+ for await (const current of decompressHandle(handle, 10, inflate)) {
290
316
  decompressed += (current as Buffer).length;
291
317
  if (decompressed > maximumDecompressedBytes) {
292
318
  inflate.destroy();
@@ -124,6 +124,7 @@ export async function runGovernorPlanner(
124
124
  if (recovered) return recovered;
125
125
  }
126
126
  let proposalWritten = false;
127
+ let invalidProposal = false;
127
128
  try {
128
129
  const reviewRunId = reviewWorkflowRunId(request.id);
129
130
  const resumingReview = Boolean(
@@ -153,14 +154,22 @@ export async function runGovernorPlanner(
153
154
  onLog: input.onLog,
154
155
  })
155
156
  ).result;
156
- const proposal = normalizeProposal(
157
- request.id,
158
- rawProposal,
159
- input.record,
160
- history.materialized_work_ids.length,
161
- request.trigger ?? "recovery",
162
- reviewInput ? persistedProposalTimestamp(reviewInput.candidate) : undefined,
163
- );
157
+ let proposal: GovernorPlanProposal;
158
+ try {
159
+ proposal = normalizeProposal(
160
+ request.id,
161
+ rawProposal,
162
+ input.record,
163
+ history.materialized_work_ids.length,
164
+ request.trigger ?? "recovery",
165
+ reviewInput ? persistedProposalTimestamp(reviewInput.candidate) : undefined,
166
+ );
167
+ } catch (error) {
168
+ // A fresh invalid proposal can consume a replan without aborting the run.
169
+ // Corrupt persisted review input still requires investigation.
170
+ invalidProposal = !reviewInput;
171
+ throw error;
172
+ }
164
173
  if (policy.review) {
165
174
  if (proposal.decision === "attention") {
166
175
  appendPlanEvent(coordRoot, input.record.intent.id, request.id, {
@@ -258,6 +267,9 @@ export async function runGovernorPlanner(
258
267
  reason: bounded((error as Error).message, "plan failure", MAX_REASON),
259
268
  ...(failureClass ? { class: failureClass } : {}),
260
269
  });
270
+ if (invalidProposal) {
271
+ return outcome(readGovernorPlan(coordRoot, input.record.intent.id, request.id));
272
+ }
261
273
  throw error;
262
274
  }
263
275
  }
@@ -3,12 +3,19 @@ import { hostname } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { coordEnv } from "../../lib/env.ts";
6
+ import {
7
+ type ProcessGenerationVerdict,
8
+ verifyProcessGeneration,
9
+ } from "../agents/state/proc-start.ts";
10
+ import { currentHarneryRuntimeBuild } from "../events/v3/runtime-build.ts";
6
11
  import { createStorageCatalog } from "../storage/catalog.ts";
7
12
  import type { HarneryLogRecordV1 } from "../storage/jsonl.ts";
8
13
  import { parseLogRecord } from "../storage/jsonl.ts";
9
14
  import { familyLogDirectory, readSegmentManifest } from "../storage/segments.ts";
10
15
  import type {
16
+ GovernorServiceBuild,
11
17
  GovernorServiceConfig,
18
+ GovernorServiceProcessDependencies,
12
19
  GovernorServiceRuntime,
13
20
  GovernorServiceStatus,
14
21
  GovernorServiceStatusRecord,
@@ -17,7 +24,106 @@ import type {
17
24
  const MAX_FILE_BYTES = 512 * 1024;
18
25
  const MAX_LOG_RECORDS = 10_000;
19
26
  const MAX_GOALS = 100;
20
- const FOREIGN_STATUS_STALE_MS = 30_000;
27
+
28
+ export function currentGovernorServiceBuild(): GovernorServiceBuild {
29
+ let version = "unknown";
30
+ let revision = "unknown";
31
+ try {
32
+ const manifest = JSON.parse(
33
+ readFileSync(new URL("../../../package.json", import.meta.url), "utf8"),
34
+ ) as {
35
+ version?: unknown;
36
+ };
37
+ if (typeof manifest.version === "string" && manifest.version.length <= 100) {
38
+ version = manifest.version;
39
+ }
40
+ } catch {
41
+ // Missing package metadata cannot establish that the running build is current.
42
+ }
43
+ try {
44
+ revision = currentHarneryRuntimeBuild();
45
+ } catch {
46
+ // Status still reports the unavailable build instead of guessing a match.
47
+ }
48
+ return { version, revision };
49
+ }
50
+
51
+ export function governorServiceBuildStatus(
52
+ record: GovernorServiceStatusRecord | undefined,
53
+ ): Pick<
54
+ GovernorServiceStatus,
55
+ "running_build" | "installed_build" | "restart_needed" | "build_status"
56
+ > {
57
+ const installed_build = currentGovernorServiceBuild();
58
+ const candidate = record?.running_build;
59
+ const running_build =
60
+ candidate &&
61
+ typeof candidate.version === "string" &&
62
+ candidate.version.length > 0 &&
63
+ candidate.version.length <= 100 &&
64
+ typeof candidate.revision === "string" &&
65
+ candidate.revision.length > 0 &&
66
+ candidate.revision.length <= 100 &&
67
+ candidate.version !== "unknown" &&
68
+ candidate.revision !== "unknown"
69
+ ? candidate
70
+ : null;
71
+ const restart_needed =
72
+ running_build === null ||
73
+ installed_build.version === "unknown" ||
74
+ installed_build.revision === "unknown" ||
75
+ running_build.version !== installed_build.version ||
76
+ running_build.revision !== installed_build.revision;
77
+ return {
78
+ running_build,
79
+ installed_build,
80
+ restart_needed,
81
+ build_status:
82
+ running_build === null
83
+ ? "running build unknown; restart needed"
84
+ : restart_needed
85
+ ? "restart needed"
86
+ : "current",
87
+ };
88
+ }
89
+
90
+ /** An unknown token or process probe cannot establish a running or stopped owner. */
91
+ export function observeGovernorServiceProcess(
92
+ record: Pick<GovernorServiceStatusRecord, "pid" | "host" | "state" | "start_token"> | undefined,
93
+ dependencies: GovernorServiceProcessDependencies = {},
94
+ ): { generation: ProcessGenerationVerdict; liveness: GovernorServiceStatus["liveness"] } {
95
+ if (!record) return { generation: "unknown", liveness: "stopped" };
96
+ const generation =
97
+ record.host === hostname()
98
+ ? verifyProcessGeneration(record.pid, record.start_token, dependencies.readToken)
99
+ : "unknown";
100
+ if (record.host !== hostname()) return { generation, liveness: "unknown" };
101
+ try {
102
+ const present = (dependencies.probePid ?? probePid)(record.pid);
103
+ if (!present) return { generation, liveness: "stopped" };
104
+ } catch (error) {
105
+ return {
106
+ generation,
107
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
108
+ };
109
+ }
110
+ return {
111
+ generation,
112
+ liveness:
113
+ generation === "same"
114
+ ? record.state === "stopped"
115
+ ? "stopped"
116
+ : "running"
117
+ : generation === "different"
118
+ ? "stopped"
119
+ : "unknown",
120
+ };
121
+ }
122
+
123
+ function probePid(pid: number): boolean {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ }
21
127
 
22
128
  export function readGovernorServiceConfig(coordRootRaw: string): GovernorServiceConfig {
23
129
  const coordRoot = resolve(coordRootRaw);
@@ -67,7 +173,10 @@ export function readGovernorServiceRuntime(
67
173
  return runtime;
68
174
  }
69
175
 
70
- export function readGovernorServiceStatus(coordRootRaw: string): GovernorServiceStatus {
176
+ export function readGovernorServiceStatus(
177
+ coordRootRaw: string,
178
+ dependencies: GovernorServiceProcessDependencies = {},
179
+ ): GovernorServiceStatus {
71
180
  const coordRoot = resolve(coordRootRaw);
72
181
  const record = readStatusRecord(coordRoot);
73
182
  let config: GovernorServiceConfig | undefined;
@@ -82,9 +191,19 @@ export function readGovernorServiceStatus(coordRootRaw: string): GovernorService
82
191
  } catch {
83
192
  // Runtime is recoverable and must not make the dashboard unreadable.
84
193
  }
85
- if (!record) return { running: false, stale: false, config, runtime };
86
- const running = statusOwnerIsLive(record);
87
- return { running, stale: !running && record.state !== "stopped", record, config, runtime };
194
+ const observation = observeGovernorServiceProcess(record, dependencies);
195
+ if (!record && existsSync(join(serviceDir(coordRoot), "status.json"))) {
196
+ observation.liveness = "unknown";
197
+ }
198
+ return {
199
+ running: observation.liveness === "running" && record?.state !== "stopped",
200
+ stale: observation.liveness === "stopped" && record !== undefined && record.state !== "stopped",
201
+ ...observation,
202
+ ...governorServiceBuildStatus(record),
203
+ record,
204
+ config,
205
+ runtime,
206
+ };
88
207
  }
89
208
 
90
209
  export function governorServiceLogPath(coordRoot: string): string {
@@ -418,26 +537,15 @@ function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | unde
418
537
  ) {
419
538
  return undefined;
420
539
  }
421
- return value;
540
+ return {
541
+ ...value,
542
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
543
+ };
422
544
  } catch {
423
545
  return undefined;
424
546
  }
425
547
  }
426
548
 
427
- function statusOwnerIsLive(record: GovernorServiceStatusRecord): boolean {
428
- if (record.state === "stopped") return false;
429
- if (record.host === hostname()) {
430
- try {
431
- process.kill(record.pid, 0);
432
- return true;
433
- } catch (error) {
434
- return (error as NodeJS.ErrnoException).code === "EPERM";
435
- }
436
- }
437
- const age = Date.now() - Date.parse(record.heartbeat_at);
438
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
439
- }
440
-
441
549
  function readBoundedJson<T>(path: string, label: string): T {
442
550
  if (!existsSync(path)) throw new Error(`${label} does not exist`);
443
551
  const size = statSync(path).size;
@@ -16,12 +16,24 @@ import {
16
16
  import { hostname } from "node:os";
17
17
  import { dirname, join, resolve } from "node:path";
18
18
  import { coordEnv } from "../../lib/env.ts";
19
+ import {
20
+ type ProcessGenerationSignalDependencies,
21
+ type ProcessGenerationVerdict,
22
+ processStartToken,
23
+ signalIfSameGeneration,
24
+ } from "../agents/state/proc-start.ts";
25
+ import { resolveBinName } from "../config.ts";
19
26
  import { type NormalizedPolicy, normalizePolicy, type PolicyIsolation } from "../policy/index.ts";
20
27
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
21
28
  import { stateDirMode, stateFileMode } from "../storage/modes.ts";
22
29
  import { RotatingTextSink } from "../storage/process-log.ts";
23
30
  import type { RunWorkItemInput } from "../work/index.ts";
24
31
  import { type GovernorRunReport, type GovernorStopReason, runGovernor } from "./runner.ts";
32
+ import {
33
+ currentGovernorServiceBuild,
34
+ governorServiceBuildStatus,
35
+ observeGovernorServiceProcess,
36
+ } from "./service-read.ts";
25
37
  import { type GovernorRecord, readGovernor } from "./state.ts";
26
38
 
27
39
  export const GOVERNOR_SERVICE_CONFIG_SCHEMA_VERSION = 1 as const;
@@ -31,7 +43,6 @@ export const GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION = 1 as const;
31
43
  const MAX_FILE_BYTES = 512 * 1024;
32
44
  const MAX_GOALS = 100;
33
45
  const MAX_ERROR = 2_000;
34
- const FOREIGN_STATUS_STALE_MS = 30_000;
35
46
 
36
47
  export interface GovernorServiceEngineConfig {
37
48
  default_adapter?: string;
@@ -85,9 +96,20 @@ export interface GovernorServiceRuntime {
85
96
 
86
97
  export type GovernorServiceProcessState = "starting" | "running" | "stopping" | "stopped";
87
98
 
99
+ export interface GovernorServiceBuild {
100
+ version: string;
101
+ revision: string;
102
+ }
103
+
104
+ export interface GovernorServiceProcessDependencies extends ProcessGenerationSignalDependencies {
105
+ probePid?: (pid: number) => boolean;
106
+ }
107
+
88
108
  export interface GovernorServiceStatusRecord {
89
109
  schema_version: typeof GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION;
90
110
  pid: number;
111
+ start_token?: string;
112
+ running_build?: GovernorServiceBuild;
91
113
  host: string;
92
114
  nonce: string;
93
115
  state: GovernorServiceProcessState;
@@ -105,6 +127,14 @@ export interface GovernorServiceStatusRecord {
105
127
  export interface GovernorServiceStatus {
106
128
  running: boolean;
107
129
  stale: boolean;
130
+ generation: ProcessGenerationVerdict;
131
+ liveness: "running" | "stopped" | "unknown";
132
+ running_build: GovernorServiceBuild | null;
133
+ installed_build: GovernorServiceBuild;
134
+ restart_needed: boolean;
135
+ build_status: string;
136
+ signal_sent?: boolean;
137
+ signal_verdict?: ProcessGenerationVerdict;
108
138
  record?: GovernorServiceStatusRecord;
109
139
  config?: GovernorServiceConfig;
110
140
  runtime?: GovernorServiceRuntime;
@@ -145,6 +175,8 @@ export interface RunGovernorServiceDaemonInput {
145
175
 
146
176
  interface ServiceLease {
147
177
  pid: number;
178
+ start_token?: string;
179
+ running_build?: GovernorServiceBuild;
148
180
  host: string;
149
181
  nonce: string;
150
182
  created_at: string;
@@ -155,8 +187,12 @@ export function configureGovernorService(
155
187
  ): GovernorServiceConfig {
156
188
  const coordRoot = resolve(input.coordRoot);
157
189
  const active = readStatusRecord(coordRoot);
158
- if (active && statusOwnerIsLive(active)) {
159
- throw new Error(`stop governor service pid ${active.pid} before changing its configuration`);
190
+ if (!active && existsSync(serviceStatusPath(coordRoot))) {
191
+ throw serviceStartRefusal(coordRoot, undefined, "unknown");
192
+ }
193
+ if (active) {
194
+ const liveness = statusOwnerIsLive(active);
195
+ if (liveness !== "stopped") throw serviceStartRefusal(coordRoot, active.pid, liveness);
160
196
  }
161
197
  const goalIds = unique(input.goalIds.map((value) => value.trim()).filter(Boolean));
162
198
  if (goalIds.length < 1 || goalIds.length > MAX_GOALS) {
@@ -235,7 +271,10 @@ export function readGovernorServiceRuntime(
235
271
  return runtime;
236
272
  }
237
273
 
238
- export function readGovernorServiceStatus(coordRootRaw: string): GovernorServiceStatus {
274
+ export function readGovernorServiceStatus(
275
+ coordRootRaw: string,
276
+ dependencies: GovernorServiceProcessDependencies = {},
277
+ ): GovernorServiceStatus {
239
278
  const coordRoot = resolve(coordRootRaw);
240
279
  const record = readStatusRecord(coordRoot);
241
280
  let config: GovernorServiceConfig | undefined;
@@ -250,17 +289,25 @@ export function readGovernorServiceStatus(coordRootRaw: string): GovernorService
250
289
  } catch {
251
290
  // Status remains readable when recoverable runtime state is corrupt.
252
291
  }
253
- if (!record) return { running: false, stale: false, config, runtime };
254
- const running = statusOwnerIsLive(record);
255
- return { running, stale: !running && record.state !== "stopped", record, config, runtime };
292
+ const observation = observeGovernorServiceProcess(record, dependencies);
293
+ if (!record && existsSync(serviceStatusPath(coordRoot))) observation.liveness = "unknown";
294
+ return {
295
+ running: observation.liveness === "running" && record?.state !== "stopped",
296
+ stale: observation.liveness === "stopped" && record !== undefined && record.state !== "stopped",
297
+ ...observation,
298
+ ...governorServiceBuildStatus(record),
299
+ record,
300
+ config,
301
+ runtime,
302
+ };
256
303
  }
257
304
 
258
305
  export async function spawnGovernorService(coordRootRaw: string): Promise<GovernorServiceStatus> {
259
306
  const coordRoot = resolve(coordRootRaw);
260
307
  readGovernorServiceConfig(coordRoot);
261
308
  const current = readGovernorServiceStatus(coordRoot);
262
- if (current.running) {
263
- throw new Error(`governor service is already running under pid ${current.record?.pid}`);
309
+ if (current.liveness !== "stopped") {
310
+ throw serviceStartRefusal(coordRoot, current.record?.pid, current.liveness);
264
311
  }
265
312
  mkdirSync(serviceDir(coordRoot), { recursive: true, mode: stateDirMode() });
266
313
  const sharedLogs = coordEnv("SHARED_LOGS") !== "0";
@@ -298,22 +345,33 @@ export async function spawnGovernorService(coordRootRaw: string): Promise<Govern
298
345
  throw new Error(`governor service failed to start; inspect ${governorServiceLogPath(coordRoot)}`);
299
346
  }
300
347
 
301
- export function requestGovernorServiceStop(coordRootRaw: string): GovernorServiceStatus {
348
+ export function requestGovernorServiceStop(
349
+ coordRootRaw: string,
350
+ dependencies: GovernorServiceProcessDependencies = {},
351
+ ): GovernorServiceStatus {
302
352
  const coordRoot = resolve(coordRootRaw);
303
- const status = readGovernorServiceStatus(coordRoot);
304
- if (!status.running || !status.record) return status;
353
+ const status = readGovernorServiceStatus(coordRoot, dependencies);
354
+ if ((status.record?.state === "stopped" || !status.record) && status.liveness === "stopped") {
355
+ return { ...status, signal_sent: false, signal_verdict: status.generation };
356
+ }
305
357
  writePrivateJsonAtomic(serviceStopPath(coordRoot), {
306
358
  requested_at: new Date().toISOString(),
307
359
  requested_by_pid: process.pid,
308
360
  });
309
- if (status.record.host === hostname()) {
310
- try {
311
- process.kill(status.record.pid, "SIGTERM");
312
- } catch {
313
- // The durable stop request remains for a racing or restarted daemon.
314
- }
315
- }
316
- return readGovernorServiceStatus(coordRoot);
361
+ const signal =
362
+ status.record?.host === hostname() && status.record.state !== "stopped"
363
+ ? signalIfSameGeneration(
364
+ status.record.pid,
365
+ status.record.start_token,
366
+ "SIGTERM",
367
+ dependencies,
368
+ )
369
+ : { sent: false, verdict: status.generation };
370
+ return {
371
+ ...readGovernorServiceStatus(coordRoot, dependencies),
372
+ signal_sent: signal.sent,
373
+ signal_verdict: signal.verdict,
374
+ };
317
375
  }
318
376
 
319
377
  export async function runGovernorServiceSweep(
@@ -461,12 +519,22 @@ export async function runGovernorServiceDaemon(
461
519
  ): Promise<GovernorServiceStatusRecord> {
462
520
  const coordRoot = resolve(input.coordRoot);
463
521
  const config = readGovernorServiceConfig(coordRoot);
522
+ const existing = readStatusRecord(coordRoot);
523
+ if (!existing && existsSync(serviceStatusPath(coordRoot))) {
524
+ throw serviceStartRefusal(coordRoot, undefined, "unknown");
525
+ }
526
+ if (existing) {
527
+ const liveness = statusOwnerIsLive(existing);
528
+ if (liveness !== "stopped") throw serviceStartRefusal(coordRoot, existing.pid, liveness);
529
+ }
464
530
  const release = acquireServiceLease(coordRoot);
465
531
  rmSync(serviceStopPath(coordRoot), { force: true });
466
532
  const now = new Date().toISOString();
467
533
  const status: GovernorServiceStatusRecord = {
468
534
  schema_version: GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION,
469
535
  pid: process.pid,
536
+ start_token: processStartToken(process.pid) ?? undefined,
537
+ running_build: currentGovernorServiceBuild(),
470
538
  host: hostname(),
471
539
  nonce: randomUUID(),
472
540
  state: "starting",
@@ -493,6 +561,8 @@ export async function runGovernorServiceDaemon(
493
561
  appendGovernorServiceDiagnostic(coordRoot, {
494
562
  event: "service.started",
495
563
  pid: process.pid,
564
+ start_token: status.start_token,
565
+ running_build: status.running_build,
496
566
  goals: config.goal_ids,
497
567
  config_created_at: config.created_at,
498
568
  });
@@ -716,6 +786,8 @@ function acquireServiceLease(coordRoot: string): () => void {
716
786
  mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
717
787
  const owner: ServiceLease = {
718
788
  pid: process.pid,
789
+ start_token: processStartToken(process.pid) ?? undefined,
790
+ running_build: currentGovernorServiceBuild(),
719
791
  host: hostname(),
720
792
  nonce: randomUUID(),
721
793
  created_at: new Date().toISOString(),
@@ -736,8 +808,9 @@ function acquireServiceLease(coordRoot: string): () => void {
736
808
  };
737
809
  if (!acquire()) {
738
810
  const existing = readLease(path);
739
- if (existing && leaseOwnerIsLive(existing)) {
740
- throw new Error(`governor service is already running under pid ${existing.pid}`);
811
+ const liveness = existing ? leaseOwnerIsLive(existing) : "unknown";
812
+ if (liveness !== "stopped") {
813
+ throw serviceStartRefusal(coordRoot, existing?.pid, liveness);
741
814
  }
742
815
  unlinkSync(path);
743
816
  if (!acquire()) throw new Error("governor service lease raced with another process");
@@ -760,19 +833,18 @@ function readLease(path: string): ServiceLease | null {
760
833
  typeof value.host === "string" &&
761
834
  typeof value.nonce === "string" &&
762
835
  typeof value.created_at === "string"
763
- ? (value as ServiceLease)
836
+ ? ({
837
+ ...value,
838
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
839
+ } as ServiceLease)
764
840
  : null;
765
841
  } catch {
766
842
  return null;
767
843
  }
768
844
  }
769
845
 
770
- function leaseOwnerIsLive(lease: ServiceLease): boolean {
771
- if (lease.host !== hostname()) {
772
- const age = Date.now() - Date.parse(lease.created_at);
773
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
774
- }
775
- return pidAlive(lease.pid);
846
+ function leaseOwnerIsLive(lease: ServiceLease): GovernorServiceStatus["liveness"] {
847
+ return observeGovernorServiceProcess({ ...lease, state: "running" }).liveness;
776
848
  }
777
849
 
778
850
  function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | undefined {
@@ -793,26 +865,31 @@ function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | unde
793
865
  ) {
794
866
  return undefined;
795
867
  }
796
- return value;
868
+ return {
869
+ ...value,
870
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
871
+ };
797
872
  } catch {
798
873
  return undefined;
799
874
  }
800
875
  }
801
876
 
802
- function statusOwnerIsLive(record: GovernorServiceStatusRecord): boolean {
803
- if (record.state === "stopped") return false;
804
- if (record.host === hostname()) return pidAlive(record.pid);
805
- const age = Date.now() - Date.parse(record.heartbeat_at);
806
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
877
+ function statusOwnerIsLive(record: GovernorServiceStatusRecord): GovernorServiceStatus["liveness"] {
878
+ return observeGovernorServiceProcess(record).liveness;
807
879
  }
808
880
 
809
- function pidAlive(pid: number): boolean {
810
- try {
811
- process.kill(pid, 0);
812
- return true;
813
- } catch (error) {
814
- return (error as NodeJS.ErrnoException).code === "EPERM";
815
- }
881
+ function serviceStartRefusal(
882
+ coordRoot: string,
883
+ pid: number | undefined,
884
+ liveness: GovernorServiceStatus["liveness"],
885
+ ): Error {
886
+ const reason =
887
+ liveness === "unknown"
888
+ ? `governor service process generation is unknown${pid ? ` for pid ${pid}` : ""}`
889
+ : `governor service process is still running under pid ${pid}`;
890
+ return new Error(
891
+ `${reason}; run \`${resolveBinName(coordRoot)} governor service stop\` to write a durable stop request before starting or reconfiguring it`,
892
+ );
816
893
  }
817
894
 
818
895
  export function appendGovernorServiceDiagnostic(