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
@@ -20,6 +20,14 @@ import {
20
20
  readLiveCoordinationRow,
21
21
  readLiveCoordinationRows,
22
22
  } from "../agents/state/live-coordination-view.ts";
23
+ import {
24
+ readCoordinationViewV3,
25
+ requireAuthoritySafeCoordinationViewV3,
26
+ } from "../events/v3/coordination-view.ts";
27
+ import {
28
+ liveInstanceIdV3,
29
+ observeLiveEventLedgerRouteV3,
30
+ } from "../events/v3/live-route-observer.ts";
23
31
 
24
32
  /**
25
33
  * Agent journal: per-agent markdown journal at `.harnery/journal/<instance_id>.md`.
@@ -421,11 +429,20 @@ export function sweepOrphanJournals(): string[] {
421
429
  const dir = journalDir();
422
430
  const root = monorepoRoot();
423
431
  const archived: string[] = [];
432
+ if (!root) return archived;
433
+ let view: ReturnType<typeof readCoordinationViewV3>;
434
+ try {
435
+ if (observeLiveEventLedgerRouteV3(root).state === "blocked") return archived;
436
+ view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(root));
437
+ if (!view.source_complete) return archived;
438
+ } catch {
439
+ // A missing or unsafe read does not prove that an owner has ended.
440
+ return archived;
441
+ }
424
442
  for (const f of readdirSync(dir)) {
425
443
  if (!f.endsWith(".md")) continue;
426
444
  const instanceId = f.replace(/\.md$/, "");
427
- const hb = root ? readLiveCoordinationRow(root, instanceId) : null;
428
- if (!hb) {
445
+ if (!view.instances[liveInstanceIdV3(instanceId)]) {
429
446
  const dest = archiveJournal(instanceId);
430
447
  if (dest) archived.push(dest);
431
448
  }
@@ -1,3 +1,5 @@
1
+ import type { ProcessGenerationVerdict } from "../agents/state/proc-start.ts";
2
+
1
3
  export const RESOURCE_SNAPSHOT_SCHEMA_VERSION = 2;
2
4
  export const RESOURCE_SERVICE_STATUS_SCHEMA_VERSION = 1;
3
5
 
@@ -154,6 +156,8 @@ export interface ResourceServiceStatusRecord {
154
156
  export interface ResourceServiceStatus {
155
157
  running: boolean;
156
158
  stale: boolean;
159
+ generation: ProcessGenerationVerdict;
160
+ liveness: "running" | "stopped" | "unknown";
157
161
  record?: ResourceServiceStatusRecord;
158
162
  status_path: string;
159
163
  snapshot_path: string;
@@ -1,5 +1,9 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { hostname } from "node:os";
2
- import { checkPidToken } from "../agents/state/proc-start.ts";
3
+ import {
4
+ type ProcessGenerationVerdict,
5
+ verifyProcessGeneration,
6
+ } from "../agents/state/proc-start.ts";
3
7
  import {
4
8
  RESOURCE_SERVICE_STATUS_SCHEMA_VERSION,
5
9
  type ResourceServiceStatus,
@@ -9,9 +13,15 @@ import { readResourceServiceRecord, resourcePaths } from "./storage.ts";
9
13
 
10
14
  const STALE_HEARTBEAT_MS = 15_000;
11
15
 
16
+ export interface ResourceProcessDependencies {
17
+ readToken?: (pid: number) => string | null;
18
+ probePid?: (pid: number) => unknown;
19
+ }
20
+
12
21
  export function readResourceServiceStatus(
13
22
  coordRoot: string,
14
23
  nowMs = Date.now(),
24
+ dependencies: ResourceProcessDependencies = {},
15
25
  ): ResourceServiceStatus {
16
26
  const paths = resourcePaths(coordRoot);
17
27
  const record = readResourceServiceRecord(coordRoot);
@@ -19,21 +29,22 @@ export function readResourceServiceStatus(
19
29
  return {
20
30
  running: false,
21
31
  stale: false,
32
+ generation: "unknown",
33
+ liveness: existsSync(paths.service) ? "unknown" : "stopped",
22
34
  status_path: paths.service,
23
35
  snapshot_path: paths.snapshot,
24
36
  };
25
37
  }
26
38
  const heartbeatAt = Date.parse(record.heartbeat_at);
27
39
  const stale = !Number.isFinite(heartbeatAt) || nowMs - heartbeatAt > STALE_HEARTBEAT_MS;
28
- const local = record.host === hostname();
29
- const running =
30
- local &&
31
- !stale &&
32
- (record.state === "starting" || record.state === "running" || record.state === "stopping") &&
33
- pidStillMatches(record.pid, record.start_token);
40
+ const owner = resourceProcessLiveness(record, dependencies);
41
+ const liveness =
42
+ owner.generation === "same" && record.state === "stopped" ? "stopped" : owner.liveness;
34
43
  return {
35
- running,
44
+ running: liveness === "running" && !stale,
36
45
  stale,
46
+ generation: owner.generation,
47
+ liveness,
37
48
  record,
38
49
  status_path: paths.service,
39
50
  snapshot_path: paths.snapshot,
@@ -52,11 +63,23 @@ function validRecord(
52
63
  );
53
64
  }
54
65
 
55
- function pidStillMatches(pid: number, startToken: string | undefined): boolean {
66
+ export function resourceProcessLiveness(
67
+ owner: { pid: number; host: string; start_token?: string },
68
+ dependencies: ResourceProcessDependencies = {},
69
+ ): { generation: ProcessGenerationVerdict; liveness: ResourceServiceStatus["liveness"] } {
70
+ if (owner.host !== hostname()) return { generation: "unknown", liveness: "unknown" };
56
71
  try {
57
- process.kill(pid, 0);
72
+ (dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid);
58
73
  } catch (error) {
59
- if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
74
+ return {
75
+ generation: "unknown",
76
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
77
+ };
60
78
  }
61
- return checkPidToken(pid, startToken) !== "mismatch";
79
+ const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
80
+ return {
81
+ generation,
82
+ liveness:
83
+ generation === "same" ? "running" : generation === "different" ? "stopped" : "unknown",
84
+ };
62
85
  }
@@ -12,7 +12,12 @@ import {
12
12
  } from "node:fs";
13
13
  import { hostname, platform } from "node:os";
14
14
  import { resolve } from "node:path";
15
- import { checkPidToken, processStartToken } from "../agents/state/proc-start.ts";
15
+ import {
16
+ type ProcessGenerationVerdict,
17
+ processStartToken,
18
+ signalIfSameGeneration,
19
+ } from "../agents/state/proc-start.ts";
20
+ import { resolveBinName } from "../config.ts";
16
21
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
17
22
  import { stateDirMode, stateFileMode } from "../storage/modes.ts";
18
23
  import {
@@ -22,7 +27,11 @@ import {
22
27
  type ResourceServiceStatusRecord,
23
28
  } from "./contract.ts";
24
29
  import { sampleResources } from "./sampler.ts";
25
- import { readResourceServiceStatus } from "./service-status.ts";
30
+ import {
31
+ type ResourceProcessDependencies,
32
+ readResourceServiceStatus,
33
+ resourceProcessLiveness,
34
+ } from "./service-status.ts";
26
35
  import { resourcePaths, writePrivateJsonAtomic } from "./storage.ts";
27
36
 
28
37
  export const RESOURCE_SERVICE_DEFAULT_INTERVAL_MS = 2_000;
@@ -56,13 +65,14 @@ export async function spawnResourceService(
56
65
  ): Promise<ResourceServiceStatus> {
57
66
  const coordRoot = resolve(coordRootRaw);
58
67
  const current = readResourceServiceStatus(coordRoot);
59
- if (current.running) {
68
+ if (current.liveness === "unknown") throw unknownResourceOwnerError();
69
+ if (current.liveness === "running") {
60
70
  throw new Error(`resource observer is already running under pid ${current.record?.pid}`);
61
71
  }
62
72
  const intervalMs = normalizeInterval(options.intervalMs);
63
73
  const paths = resourcePaths(coordRoot);
74
+ assertResourceLeaseAvailable(paths.lease);
64
75
  mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
65
- rmSync(paths.stop, { force: true });
66
76
  const harnBin = new URL("../../../bin/harn", import.meta.url).pathname;
67
77
  if (!existsSync(harnBin)) throw new Error(`cannot find harn executable at ${harnBin}`);
68
78
  let spawnError: Error | undefined;
@@ -119,29 +129,49 @@ export async function ensureResourceServiceRunning(
119
129
  }
120
130
  }
121
131
 
122
- export function requestResourceServiceStop(coordRootRaw: string): ResourceServiceStatus {
132
+ export interface ResourceServiceStopResult extends ResourceServiceStatus {
133
+ signal_sent: boolean;
134
+ signal_verdict: ProcessGenerationVerdict;
135
+ }
136
+
137
+ export function requestResourceServiceStop(
138
+ coordRootRaw: string,
139
+ dependencies: ResourceProcessDependencies & {
140
+ kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
141
+ } = {},
142
+ ): ResourceServiceStopResult {
123
143
  const coordRoot = resolve(coordRootRaw);
124
144
  const paths = resourcePaths(coordRoot);
125
- const status = readResourceServiceStatus(coordRoot);
145
+ const status = readResourceServiceStatus(coordRoot, Date.now(), dependencies);
126
146
  mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
127
147
  writePrivateJsonAtomic(paths.stop, {
128
148
  requested_at: new Date().toISOString(),
129
149
  requested_by_pid: process.pid,
130
150
  });
131
- if (status.running && status.record?.host === hostname()) {
132
- try {
133
- process.kill(status.record.pid, "SIGTERM");
134
- } catch {
135
- // The durable stop file handles a racing exit.
136
- }
137
- }
138
- return readResourceServiceStatus(coordRoot);
151
+ const signal =
152
+ status.liveness === "running" && status.record?.host === hostname()
153
+ ? signalIfSameGeneration(
154
+ status.record.pid,
155
+ status.record.start_token,
156
+ "SIGTERM",
157
+ dependencies,
158
+ )
159
+ : { sent: false, verdict: status.generation };
160
+ return {
161
+ ...readResourceServiceStatus(coordRoot, Date.now(), dependencies),
162
+ signal_sent: signal.sent,
163
+ signal_verdict: signal.verdict,
164
+ };
139
165
  }
140
166
 
141
167
  export async function runResourceService(
142
168
  input: RunResourceServiceInput,
143
169
  ): Promise<ResourceServiceStatusRecord> {
144
170
  const coordRoot = resolve(input.coordRoot);
171
+ const current = readResourceServiceStatus(coordRoot);
172
+ if (current.liveness === "unknown") throw unknownResourceOwnerError();
173
+ if (current.liveness === "running")
174
+ throw new Error(`resource observer is already running under pid ${current.record?.pid}`);
145
175
  const paths = resourcePaths(coordRoot);
146
176
  const intervalMs = normalizeInterval(input.intervalMs);
147
177
  const now = input.now ?? (() => new Date());
@@ -253,7 +283,9 @@ function acquireLease(coordRoot: string, now: Date): ResourceServiceLease {
253
283
  } catch (error) {
254
284
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
255
285
  const existing = readLease(paths.lease);
256
- if (existing && leaseIsLive(existing)) {
286
+ const liveness = existing ? leaseIsLive(existing) : "unknown";
287
+ if (liveness === "unknown") throw unknownResourceOwnerError();
288
+ if (existing && liveness === "running") {
257
289
  throw new Error(`resource observer lease is held by pid ${existing.pid}`);
258
290
  }
259
291
  try {
@@ -279,7 +311,13 @@ function releaseLease(path: string, nonce: string): void {
279
311
  function readLease(path: string): ResourceServiceLease | undefined {
280
312
  try {
281
313
  const value = JSON.parse(readFileSync(path, "utf8")) as ResourceServiceLease;
282
- return value && Number.isInteger(value.pid) && typeof value.nonce === "string"
314
+ return value &&
315
+ Number.isSafeInteger(value.pid) &&
316
+ value.pid > 0 &&
317
+ typeof value.host === "string" &&
318
+ value.host.length > 0 &&
319
+ typeof value.nonce === "string" &&
320
+ value.nonce.length > 0
283
321
  ? value
284
322
  : undefined;
285
323
  } catch {
@@ -287,14 +325,23 @@ function readLease(path: string): ResourceServiceLease | undefined {
287
325
  }
288
326
  }
289
327
 
290
- function leaseIsLive(lease: ResourceServiceLease): boolean {
291
- if (lease.host !== hostname()) return true;
292
- try {
293
- process.kill(lease.pid, 0);
294
- } catch (error) {
295
- if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
296
- }
297
- return checkPidToken(lease.pid, lease.start_token) !== "mismatch";
328
+ function leaseIsLive(lease: ResourceServiceLease): ResourceServiceStatus["liveness"] {
329
+ return resourceProcessLiveness(lease).liveness;
330
+ }
331
+
332
+ function assertResourceLeaseAvailable(path: string): void {
333
+ if (!existsSync(path)) return;
334
+ const lease = readLease(path);
335
+ const liveness = lease ? leaseIsLive(lease) : "unknown";
336
+ if (liveness === "unknown") throw unknownResourceOwnerError();
337
+ if (liveness === "running")
338
+ throw new Error(`resource observer lease is held by pid ${lease?.pid}`);
339
+ }
340
+
341
+ function unknownResourceOwnerError(): Error {
342
+ return new Error(
343
+ `resource observer process identity is unknown; run ${resolveBinName()} supervisor stop to write the durable stop request before starting again`,
344
+ );
298
345
  }
299
346
 
300
347
  function log(
@@ -10,8 +10,9 @@ import {
10
10
  SUPERVISOR_PRESSURE_SCHEMA_VERSION,
11
11
  type SupervisorCapability,
12
12
  type SupervisorPressureRecord,
13
+ type SupervisorStatus,
13
14
  } from "../supervisor/contract.ts";
14
- import { readSupervisorStatus } from "../supervisor/status.ts";
15
+ import { installedSupervisorBuild, readSupervisorStatus } from "../supervisor/status.ts";
15
16
  import { supervisorPaths } from "../supervisor/storage.ts";
16
17
  import {
17
18
  RESOURCE_SNAPSHOT_SCHEMA_VERSION,
@@ -43,7 +44,17 @@ export interface ResourceStatus {
43
44
  platform: NodeJS.Platform | null;
44
45
  namespace: ResourceSnapshot["namespace"];
45
46
  support: ResourceSnapshot["support"] | null;
46
- writer: { running: boolean; stale: boolean };
47
+ writer: Pick<
48
+ SupervisorStatus,
49
+ | "running"
50
+ | "stale"
51
+ | "generation"
52
+ | "liveness"
53
+ | "running_build"
54
+ | "installed_build"
55
+ | "restart_needed"
56
+ | "build_status"
57
+ >;
47
58
  /**
48
59
  * The one published pressure assessment, read from the observer's record so
49
60
  * this surface, the prompt notice, the dashboard, and a diagnostic bundle all
@@ -222,12 +233,16 @@ export function formatResourceSummary(status: ResourceStatus): string {
222
233
  ? "unknown"
223
234
  : `${formatBytes(m.memory_available_bytes)} available`;
224
235
  const diskText = disk ? `${formatBytes(disk.available_bytes as number)} available` : "unknown";
225
- return `${lead} ${status.namespace}/${status.platform}: CPU ${cpu}, RAM ${ram}, disk ${diskText}; ${Math.round((status.sample_age_ms ?? 0) / 1_000)}s old${status.support?.state === "partial" ? "; partial" : ""}${!status.writer.running ? "; writer stopped" : ""}.`;
236
+ return `${lead} ${status.namespace}/${status.platform}: CPU ${cpu}, RAM ${ram}, disk ${diskText}; ${Math.round((status.sample_age_ms ?? 0) / 1_000)}s old${status.support?.state === "partial" ? "; partial" : ""}${!status.writer.running ? `; writer ${status.writer.liveness === "unknown" ? "unknown" : status.writer.stale ? "stale" : "stopped"}` : ""}.`;
226
237
  }
227
238
 
228
239
  export function formatResourceStatus(status: ResourceStatus, bin: string): string {
229
240
  const a = status.assessment;
230
241
  const lines = [formatResourceSummary(status)];
242
+ const writer = status.writer;
243
+ lines.push(
244
+ `Writer: ${writer.liveness}; generation ${writer.generation}; running build ${writer.running_build ? `${writer.running_build.version} (${writer.running_build.revision})` : "unknown"}; installed build ${writer.installed_build.version} (${writer.installed_build.revision}); ${writer.build_status}.`,
245
+ );
231
246
  lines.push(
232
247
  `Assessment: ${a.state} at ${a.scope} scope; limiting resource ${a.limiting_resource}; trend ${a.trend}; evidence ${a.evidence_state}; recommended action ${a.recommended_action}.`,
233
248
  );
@@ -247,7 +262,11 @@ export function formatResourceStatus(status: ResourceStatus, bin: string): strin
247
262
  lines.push(
248
263
  `Unattributed memory: ${Math.round(a.unattributed_memory_percent)}% of machine memory has no validated owner.`,
249
264
  );
250
- if (status.state !== "fresh" || !status.writer.running)
265
+ if (status.writer.liveness === "unknown")
266
+ lines.push(
267
+ `Inspect the writer with ${bin} supervisor status. Run ${bin} supervisor stop to write its durable stop request before starting again.`,
268
+ );
269
+ else if (status.state !== "fresh" || !status.writer.running)
251
270
  lines.push(
252
271
  `Start or inspect the writer with ${bin} supervisor start --keep-alive and ${bin} supervisor status.`,
253
272
  );
@@ -345,7 +364,15 @@ export function readPublishedPressure(
345
364
  });
346
365
  // A record can only be trusted while its writer is alive. Every surface makes
347
366
  // the same check here so none of them reports a state the others do not.
348
- if (!(writer ?? readWriterState(coordRoot, nowMs)).running) {
367
+ const observer = writer ?? readWriterState(coordRoot, nowMs);
368
+ if (observer.liveness === "unknown") {
369
+ return gap(
370
+ "error",
371
+ "pressure_observer_unknown",
372
+ "Local resource pressure cannot be determined because the resource observer's process identity is unknown.",
373
+ );
374
+ }
375
+ if (!observer.running) {
349
376
  return gap(
350
377
  "expired",
351
378
  "pressure_observer_not_running",
@@ -407,7 +434,7 @@ function readWriterState(coordRoot: string, nowMs: number): ResourceStatus["writ
407
434
  // Keep malformed service metadata from making the read expensive. The shared
408
435
  // status reader retains ownership of PID/start-token liveness checks.
409
436
  if (statSync(supervisorPaths(coordRoot).service).size > 65_536) {
410
- return { running: false, stale: false };
437
+ return unknownWriterState();
411
438
  }
412
439
  const writer = readSupervisorStatus(coordRoot, nowMs);
413
440
  const futureHeartbeat =
@@ -415,13 +442,35 @@ function readWriterState(coordRoot: string, nowMs: number): ResourceStatus["writ
415
442
  return {
416
443
  running: writer.running && !futureHeartbeat,
417
444
  stale: writer.stale || !!futureHeartbeat,
445
+ generation: writer.generation,
446
+ liveness: writer.liveness,
447
+ running_build: writer.running_build,
448
+ installed_build: writer.installed_build,
449
+ restart_needed: writer.restart_needed,
450
+ build_status: writer.build_status,
418
451
  };
419
- } catch {
452
+ } catch (error) {
420
453
  // A missing writer must not break normal coordination.
421
- return { running: false, stale: false };
454
+ return {
455
+ ...unknownWriterState(),
456
+ liveness: (error as NodeJS.ErrnoException).code === "ENOENT" ? "stopped" : "unknown",
457
+ };
422
458
  }
423
459
  }
424
460
 
461
+ function unknownWriterState(): ResourceStatus["writer"] {
462
+ return {
463
+ running: false,
464
+ stale: false,
465
+ generation: "unknown",
466
+ liveness: "unknown",
467
+ running_build: null,
468
+ installed_build: installedSupervisorBuild(),
469
+ restart_needed: true,
470
+ build_status: "running build unknown; restart needed",
471
+ };
472
+ }
473
+
425
474
  function validPressureRecord(v: unknown): v is SupervisorPressureRecord {
426
475
  if (
427
476
  !object(v) ||