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,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { CoordinationHealthSnapshot } from "../agents/health.ts";
3
+ import { canonicalizeDiagnosticIdentity } from "../diagnostics/path-identity.ts";
3
4
  import type {
4
5
  ResourceProcessGroup,
5
6
  ResourceProcessSample,
@@ -96,15 +97,17 @@ export function updateSupervisorFindings(
96
97
  const candidates = evaluateCandidates(input)
97
98
  .sort((left, right) => severityRank(right.severity) - severityRank(left.severity))
98
99
  .slice(0, SUPERVISOR_DIAGNOSTIC_LIMITS.max_active_findings);
99
- const priorActive = new Map(previous.active.map((entry) => [entry.fingerprint, entry]));
100
+ const priorActive = new Map(previous.active.map((entry) => [findingFingerprint(entry), entry]));
100
101
  const current = new Set<string>();
101
102
  const active: SupervisorFinding[] = [];
102
103
  const transitions = new Map(previous.transitions.map((entry) => [entry.id, entry]));
103
104
 
104
- for (const candidate of candidates) {
105
- const fingerprint = findingFingerprint(candidate);
106
- current.add(fingerprint);
107
- const prior = priorActive.get(fingerprint) ?? recentResolved(previous, fingerprint, observedAt);
105
+ for (const observed of candidates) {
106
+ const identity = findingFingerprint(observed);
107
+ current.add(identity);
108
+ const prior = priorActive.get(identity) ?? recentResolved(previous, identity, observedAt);
109
+ const candidate = preserveEpisodeReferences(observed, prior);
110
+ const fingerprint = prior?.fingerprint ?? identity;
108
111
  const openedAt = prior?.opened_at ?? candidate.primary_source.observed_at;
109
112
  const occurrenceCount = prior
110
113
  ? prior.state === "resolved"
@@ -114,7 +117,7 @@ export function updateSupervisorFindings(
114
117
  const peak = peakEvidence(prior, candidate.evidence, observedAt);
115
118
  const finding: SupervisorFinding = {
116
119
  schema_version: SUPERVISOR_FINDING_SCHEMA_VERSION,
117
- id: deterministicId("find", `${fingerprint}\u0000${openedAt}`),
120
+ id: prior?.id ?? deterministicId("find", `${fingerprint}\u0000${openedAt}`),
118
121
  fingerprint,
119
122
  source_kind: candidate.source_kind,
120
123
  finding_kind: candidate.finding_kind,
@@ -139,7 +142,7 @@ export function updateSupervisorFindings(
139
142
  }
140
143
 
141
144
  for (const prior of priorActive.values()) {
142
- if (current.has(prior.fingerprint)) continue;
145
+ if (current.has(findingFingerprint(prior))) continue;
143
146
  transitions.set(prior.id, {
144
147
  ...prior,
145
148
  state: "resolved",
@@ -859,7 +862,7 @@ function recentResolved(
859
862
  return findings.transitions
860
863
  .filter(
861
864
  (entry) =>
862
- entry.fingerprint === fingerprint &&
865
+ findingFingerprint(entry) === fingerprint &&
863
866
  entry.state === "resolved" &&
864
867
  entry.resolved_at !== undefined &&
865
868
  observedMs - Date.parse(entry.resolved_at) >= 0 &&
@@ -898,7 +901,12 @@ function peakEvidence(
898
901
  : {};
899
902
  }
900
903
 
901
- function findingFingerprint(candidate: Candidate): string {
904
+ function findingFingerprint(
905
+ candidate: Pick<
906
+ Candidate,
907
+ "source_kind" | "finding_kind" | "scope_kind" | "scope_id" | "primary_source"
908
+ >,
909
+ ): string {
902
910
  return digest(
903
911
  [
904
912
  candidate.source_kind,
@@ -915,7 +923,57 @@ function deterministicId(prefix: string, value: string): string {
915
923
  }
916
924
 
917
925
  function digest(value: string): string {
918
- return createHash("sha256").update(value).digest("hex");
926
+ return createHash("sha256").update(canonicalizeDiagnosticIdentity(value)).digest("hex");
927
+ }
928
+
929
+ /** Keep opaque episode links when the same source observation is replayed privately. */
930
+ function preserveEpisodeReferences(candidate: Candidate, prior?: SupervisorFinding): Candidate {
931
+ if (!prior) return candidate;
932
+ const sources = [
933
+ prior.primary_source,
934
+ ...prior.evidence.map((entry) => entry.source),
935
+ ...(prior.workload_context ? [prior.workload_context.source] : []),
936
+ ];
937
+ const byIdentity = new Map(sources.map((source) => [sourceIdentity(source), source]));
938
+ const retainSource = (source: SupervisorSourceReference): SupervisorSourceReference => {
939
+ const captured = byIdentity.get(sourceIdentity(source));
940
+ return captured ? { ...source, id: captured.id } : source;
941
+ };
942
+ return {
943
+ ...candidate,
944
+ primary_source: retainSource(candidate.primary_source),
945
+ evidence: candidate.evidence.map((entry) => {
946
+ const captured = prior.evidence.find(
947
+ (previous) =>
948
+ sourceIdentity(previous.source) === sourceIdentity(entry.source) &&
949
+ previous.unit === entry.unit &&
950
+ previous.observed_value === entry.observed_value &&
951
+ canonicalizeDiagnosticIdentity(previous.summary) ===
952
+ canonicalizeDiagnosticIdentity(entry.summary),
953
+ );
954
+ return {
955
+ ...entry,
956
+ ...(captured ? { id: captured.id } : {}),
957
+ source: retainSource(entry.source),
958
+ };
959
+ }),
960
+ ...(candidate.workload_context
961
+ ? {
962
+ workload_context: {
963
+ ...candidate.workload_context,
964
+ source: retainSource(candidate.workload_context.source),
965
+ },
966
+ }
967
+ : {}),
968
+ };
969
+ }
970
+
971
+ function sourceIdentity(source: SupervisorSourceReference): string {
972
+ return canonicalizeDiagnosticIdentity(
973
+ [source.source_kind, source.source_id, source.record_id ?? "", source.observed_at].join(
974
+ "\u0000",
975
+ ),
976
+ );
919
977
  }
920
978
 
921
979
  function validFindings(value: SupervisorFindings | undefined): value is SupervisorFindings {
@@ -16,7 +16,12 @@ import { resolve } from "node:path";
16
16
  import { performance } from "node:perf_hooks";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { collectCoordinationHealthSnapshot } from "../agents/health.ts";
19
- import { checkPidToken, processStartToken } from "../agents/state/proc-start.ts";
19
+ import {
20
+ type ProcessGenerationVerdict,
21
+ processStartToken,
22
+ signalIfSameGeneration,
23
+ } from "../agents/state/proc-start.ts";
24
+ import { resolveBinName } from "../config.ts";
20
25
  import { pressureHistoryFromSupervisor } from "../diagnostics/advice.ts";
21
26
  import { assessPressure } from "../diagnostics/pressure.ts";
22
27
  import { readEventV3ControlState } from "../events/v3/control.ts";
@@ -52,7 +57,12 @@ import type {
52
57
  } from "./ledger-state.ts";
53
58
  import { SupervisorLogCollector } from "./log-feed.ts";
54
59
  import { collectServiceHealth } from "./services.ts";
55
- import { readSupervisorStatus } from "./status.ts";
60
+ import {
61
+ installedSupervisorBuild,
62
+ readSupervisorStatus,
63
+ type SupervisorProcessDependencies,
64
+ supervisorProcessLiveness,
65
+ } from "./status.ts";
56
66
  import {
57
67
  readSupervisorFindings,
58
68
  readSupervisorHistory,
@@ -97,15 +107,16 @@ export async function spawnSupervisor(
97
107
  ): Promise<SupervisorStatus> {
98
108
  const coordRoot = resolve(coordRootRaw);
99
109
  const current = readSupervisorStatus(coordRoot);
100
- if (current.running) {
110
+ if (current.liveness === "unknown") throw unknownOwnerError();
111
+ if (current.liveness === "running") {
101
112
  throw new Error(`local supervisor is already running under pid ${current.record?.pid}`);
102
113
  }
114
+ const paths = supervisorPaths(coordRoot);
115
+ assertLeaseAvailable(paths.lease);
103
116
  await stopLegacyResourceObserver(coordRoot);
104
117
  const intervalMs = normalizeInterval(options.intervalMs);
105
118
  const idleExitMs = normalizeIdleExit(options.idleExitMs);
106
- const paths = supervisorPaths(coordRoot);
107
119
  mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
108
- rmSync(paths.stop, { force: true });
109
120
  const cliPath = fileURLToPath(
110
121
  new URL(import.meta.url.endsWith(".ts") ? "../../cli.ts" : "../../cli.js", import.meta.url),
111
122
  );
@@ -167,29 +178,57 @@ export async function ensureSupervisorRunning(coordRoot: string): Promise<Ensure
167
178
  }
168
179
  }
169
180
 
170
- export function requestSupervisorStop(coordRootRaw: string): SupervisorStatus {
181
+ export interface SupervisorStopResult extends SupervisorStatus {
182
+ signal_sent: boolean;
183
+ signal_verdict: ProcessGenerationVerdict;
184
+ }
185
+
186
+ export function requestSupervisorStop(
187
+ coordRootRaw: string,
188
+ dependencies: SupervisorProcessDependencies & {
189
+ kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
190
+ } = {},
191
+ ): SupervisorStopResult {
171
192
  const coordRoot = resolve(coordRootRaw);
172
193
  const paths = supervisorPaths(coordRoot);
173
- const status = readSupervisorStatus(coordRoot);
194
+ const status = readSupervisorStatus(coordRoot, Date.now(), dependencies);
174
195
  mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
175
196
  writePrivateJsonAtomic(paths.stop, {
176
197
  requested_at: new Date().toISOString(),
177
198
  requested_by_pid: process.pid,
178
199
  });
179
- if (status.running && status.record?.host === hostname() && process.platform !== "win32") {
180
- try {
181
- process.kill(status.record.pid, "SIGTERM");
182
- } catch {
183
- // The durable stop file handles a racing exit.
184
- }
200
+ const legacyPaths = resourcePaths(coordRoot);
201
+ if (existsSync(legacyPaths.service) || existsSync(legacyPaths.lease))
202
+ requestResourceServiceStop(coordRoot, dependencies);
203
+ let signal = { sent: false, verdict: status.generation };
204
+ if (
205
+ status.liveness === "running" &&
206
+ status.record?.host === hostname() &&
207
+ process.platform !== "win32"
208
+ ) {
209
+ signal = signalIfSameGeneration(
210
+ status.record.pid,
211
+ status.record.start_token,
212
+ "SIGTERM",
213
+ dependencies,
214
+ );
185
215
  }
186
- return readSupervisorStatus(coordRoot);
216
+ return {
217
+ ...readSupervisorStatus(coordRoot, Date.now(), dependencies),
218
+ signal_sent: signal.sent,
219
+ signal_verdict: signal.verdict,
220
+ };
187
221
  }
188
222
 
189
223
  export async function runSupervisor(
190
224
  input: RunSupervisorInput,
191
225
  ): Promise<SupervisorServiceStatusRecord> {
192
226
  const coordRoot = resolve(input.coordRoot);
227
+ const current = readSupervisorStatus(coordRoot);
228
+ if (current.liveness === "unknown") throw unknownOwnerError();
229
+ if (current.liveness === "running") {
230
+ throw new Error(`local supervisor is already running under pid ${current.record?.pid}`);
231
+ }
193
232
  const paths = supervisorPaths(coordRoot);
194
233
  const intervalMs = normalizeInterval(input.intervalMs);
195
234
  const idleExitMs = normalizeIdleExit(input.idleExitMs);
@@ -204,6 +243,7 @@ export async function runSupervisor(
204
243
  schema_version: SUPERVISOR_STATUS_SCHEMA_VERSION,
205
244
  pid: process.pid,
206
245
  ...(startToken ? { start_token: startToken } : {}),
246
+ running_build: installedSupervisorBuild(),
207
247
  host: hostname(),
208
248
  nonce: lease.nonce,
209
249
  state: "starting",
@@ -517,7 +557,9 @@ function acquireLease(coordRoot: string, now: Date): SupervisorLease {
517
557
  } catch (error) {
518
558
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
519
559
  const existing = readLease(paths.lease);
520
- if (existing && leaseIsLive(existing)) {
560
+ const liveness = existing ? leaseIsLive(existing) : "unknown";
561
+ if (liveness === "unknown") throw unknownOwnerError();
562
+ if (existing && liveness === "running") {
521
563
  throw new Error(`local supervisor lease is held by pid ${existing.pid}`);
522
564
  }
523
565
  try {
@@ -554,7 +596,13 @@ function releaseLease(path: string, nonce: string): void {
554
596
  function readLease(path: string): SupervisorLease | undefined {
555
597
  try {
556
598
  const value = JSON.parse(readFileSync(path, "utf8")) as SupervisorLease;
557
- return value && Number.isSafeInteger(value.pid) && typeof value.nonce === "string"
599
+ return value &&
600
+ Number.isSafeInteger(value.pid) &&
601
+ value.pid > 0 &&
602
+ typeof value.host === "string" &&
603
+ value.host.length > 0 &&
604
+ typeof value.nonce === "string" &&
605
+ value.nonce.length > 0
558
606
  ? value
559
607
  : undefined;
560
608
  } catch {
@@ -562,22 +610,36 @@ function readLease(path: string): SupervisorLease | undefined {
562
610
  }
563
611
  }
564
612
 
565
- function leaseIsLive(lease: SupervisorLease): boolean {
566
- if (lease.host !== hostname()) return true;
567
- try {
568
- process.kill(lease.pid, 0);
569
- } catch (error) {
570
- if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
571
- }
572
- return checkPidToken(lease.pid, lease.start_token) !== "mismatch";
613
+ function leaseIsLive(lease: SupervisorLease): SupervisorStatus["liveness"] {
614
+ return supervisorProcessLiveness(lease).liveness;
615
+ }
616
+
617
+ function assertLeaseAvailable(path: string): void {
618
+ if (!existsSync(path)) return;
619
+ const lease = readLease(path);
620
+ const liveness = lease ? leaseIsLive(lease) : "unknown";
621
+ if (liveness === "unknown") throw unknownOwnerError();
622
+ if (liveness === "running")
623
+ throw new Error(`local supervisor lease is held by pid ${lease?.pid}`);
624
+ }
625
+
626
+ function unknownOwnerError(): Error {
627
+ return new Error(
628
+ `local supervisor process identity is unknown; run ${resolveBinName()} supervisor stop to write the durable stop request before starting again`,
629
+ );
573
630
  }
574
631
 
575
632
  async function stopLegacyResourceObserver(coordRoot: string): Promise<void> {
576
633
  const legacy = readResourceServiceStatus(coordRoot);
577
- if (!legacy.running) return;
634
+ if (legacy.liveness === "stopped") return;
578
635
  requestResourceServiceStop(coordRoot);
579
636
  const deadline = Date.now() + 2_000;
580
- while (readResourceServiceStatus(coordRoot).running && Date.now() < deadline) await delay(50);
637
+ while (readResourceServiceStatus(coordRoot).liveness !== "stopped" && Date.now() < deadline)
638
+ await delay(50);
639
+ if (readResourceServiceStatus(coordRoot).liveness !== "stopped")
640
+ throw new Error(
641
+ "legacy resource observer has not stopped after its durable stop request; its process identity may be unknown",
642
+ );
581
643
  }
582
644
 
583
645
  function log(
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs
2
2
  import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { checkPidToken, processStartToken } from "../agents/state/proc-start.ts";
5
+ import { readGovernorServiceStatus } from "../governor/service-read.ts";
5
6
  import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
6
7
  import {
7
8
  type ObservedServiceHealth,
@@ -41,10 +42,7 @@ export function collectServiceHealth(
41
42
  join(coordRoot, ".harnery", "semantic", "v2", "service.json"),
42
43
  "semantic-reader",
43
44
  );
44
- const governor = readDetachedService(
45
- join(coordRoot, ".harnery", "governor-service", "service.json"),
46
- "governor",
47
- );
45
+ const governor = readGovernorHealth(coordRoot);
48
46
  const relay = readPresenceRelay(join(coordRoot, ".harnery", "presence", "relay-daemon.json"));
49
47
  const dashboard = consumers.find((consumer) => consumer.id === "dashboard");
50
48
  const services: ObservedServiceHealth[] = [
@@ -118,10 +116,7 @@ interface DetachedServiceRecord {
118
116
  heartbeat_at?: string;
119
117
  }
120
118
 
121
- function readDetachedService(
122
- path: string,
123
- id: "semantic-reader" | "governor",
124
- ): ObservedServiceHealth {
119
+ function readDetachedService(path: string, id: "semantic-reader"): ObservedServiceHealth {
125
120
  const record = readBoundedJson<DetachedServiceRecord>(path);
126
121
  if (!record || !Number.isSafeInteger(record.pid) || record.pid < 1) {
127
122
  return { id, state: "stopped" };
@@ -150,6 +145,29 @@ function readDetachedService(
150
145
  );
151
146
  }
152
147
 
148
+ function readGovernorHealth(coordRoot: string): ObservedServiceHealth {
149
+ const status = readGovernorServiceStatus(coordRoot);
150
+ const state =
151
+ status.liveness === "unknown"
152
+ ? "unknown"
153
+ : status.running
154
+ ? "running"
155
+ : status.stale
156
+ ? "stale"
157
+ : "stopped";
158
+ const reason =
159
+ state === "unknown"
160
+ ? "governor-process-generation-unknown"
161
+ : status.generation === "different"
162
+ ? "recorded-process-generation-different"
163
+ : status.stale
164
+ ? "recorded-running-process-missing"
165
+ : undefined;
166
+ return status.record
167
+ ? projection("governor", state, status.record, reason)
168
+ : { id: "governor", state, ...(reason ? { reason } : {}) };
169
+ }
170
+
153
171
  function readPresenceRelay(path: string): ObservedServiceHealth {
154
172
  const record = readBoundedJson<{ pid: number; started_at?: string }>(path);
155
173
  if (!record || !Number.isSafeInteger(record.pid) || record.pid < 1) {
@@ -1,7 +1,14 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { hostname } from "node:os";
2
- import { checkPidToken } from "../agents/state/proc-start.ts";
3
+ import { join } from "node:path";
4
+ import {
5
+ type ProcessGenerationVerdict,
6
+ verifyProcessGeneration,
7
+ } from "../agents/state/proc-start.ts";
8
+ import { currentHarneryRuntimeBuild, harneryPackageRoot } from "../events/v3/runtime-build.ts";
3
9
  import {
4
10
  SUPERVISOR_STATUS_SCHEMA_VERSION,
11
+ type SupervisorBuild,
5
12
  type SupervisorServiceStatusRecord,
6
13
  type SupervisorStatus,
7
14
  } from "./contract.ts";
@@ -9,27 +16,71 @@ import { readSupervisorServiceRecord, supervisorPaths } from "./storage.ts";
9
16
 
10
17
  const STALE_HEARTBEAT_MS = 15_000;
11
18
 
12
- export function readSupervisorStatus(coordRoot: string, nowMs = Date.now()): SupervisorStatus {
19
+ export interface SupervisorProcessDependencies {
20
+ readToken?: (pid: number) => string | null;
21
+ probePid?: (pid: number) => unknown;
22
+ }
23
+
24
+ export function installedSupervisorBuild(): SupervisorBuild {
25
+ let version = "unknown";
26
+ let revision = "unknown";
27
+ try {
28
+ const pkg = JSON.parse(readFileSync(join(harneryPackageRoot(), "package.json"), "utf8"));
29
+ if (typeof pkg.version === "string") version = pkg.version;
30
+ revision = currentHarneryRuntimeBuild();
31
+ } catch {
32
+ // Missing build evidence must still produce an inspectable status.
33
+ }
34
+ return { version, revision };
35
+ }
36
+
37
+ export function readSupervisorStatus(
38
+ coordRoot: string,
39
+ nowMs = Date.now(),
40
+ dependencies: SupervisorProcessDependencies = {},
41
+ ): SupervisorStatus {
13
42
  const paths = supervisorPaths(coordRoot);
14
43
  const record = readSupervisorServiceRecord(coordRoot);
44
+ const installedBuild = installedSupervisorBuild();
45
+ const runningBuild = validBuild(record?.running_build) ? record.running_build : null;
46
+ const restartNeeded =
47
+ !runningBuild ||
48
+ installedBuild.version === "unknown" ||
49
+ installedBuild.revision === "unknown" ||
50
+ runningBuild.version !== installedBuild.version ||
51
+ runningBuild.revision !== installedBuild.revision;
52
+ const build = {
53
+ running_build: runningBuild,
54
+ installed_build: installedBuild,
55
+ restart_needed: restartNeeded,
56
+ build_status: !runningBuild
57
+ ? "running build unknown; restart needed"
58
+ : restartNeeded
59
+ ? "restart needed"
60
+ : "current",
61
+ };
15
62
  if (!validRecord(record)) {
16
63
  return {
17
64
  running: false,
18
65
  stale: false,
66
+ generation: "unknown",
67
+ liveness: existsSync(paths.service) ? "unknown" : "stopped",
68
+ ...build,
19
69
  status_path: paths.service,
20
70
  snapshot_path: paths.snapshot,
21
71
  };
22
72
  }
23
73
  const heartbeatAt = Date.parse(record.heartbeat_at);
24
74
  const stale = !Number.isFinite(heartbeatAt) || nowMs - heartbeatAt > STALE_HEARTBEAT_MS;
25
- const running =
26
- record.host === hostname() &&
27
- !stale &&
28
- (record.state === "starting" || record.state === "running" || record.state === "stopping") &&
29
- pidStillMatches(record.pid, record.start_token);
75
+ const owner = supervisorProcessLiveness(record, dependencies);
76
+ const liveness =
77
+ owner.generation === "same" && record.state === "stopped" ? "stopped" : owner.liveness;
30
78
  return {
31
- running,
79
+ running: liveness === "running" && !stale && record.state !== "stopped",
32
80
  stale,
81
+ generation: owner.generation,
82
+ liveness,
83
+ ...build,
33
84
  record,
34
85
  status_path: paths.service,
35
86
  snapshot_path: paths.snapshot,
@@ -49,11 +100,37 @@ function validRecord(
49
100
  );
50
101
  }
51
102
 
52
- function pidStillMatches(pid: number, startToken: string | undefined): boolean {
103
+ /** A stale heartbeat does not prove that a process has exited. */
104
+ export function supervisorProcessLiveness(
105
+ owner: { pid: number; host: string; start_token?: string },
106
+ dependencies: SupervisorProcessDependencies = {},
107
+ ): { liveness: SupervisorStatus["liveness"]; generation: ProcessGenerationVerdict } {
108
+ if (owner.host !== hostname()) return { liveness: "unknown", generation: "unknown" };
53
109
  try {
54
- process.kill(pid, 0);
110
+ (dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid);
55
111
  } catch (error) {
56
- if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
112
+ return {
113
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
114
+ generation: "unknown",
115
+ };
57
116
  }
58
- return checkPidToken(pid, startToken) !== "mismatch";
117
+ const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
118
+ return {
119
+ generation,
120
+ liveness:
121
+ generation === "same" ? "running" : generation === "different" ? "stopped" : "unknown",
122
+ };
123
+ }
124
+
125
+ function validBuild(value: unknown): value is SupervisorBuild {
126
+ if (!value || typeof value !== "object") return false;
127
+ const build = value as Partial<SupervisorBuild>;
128
+ return (
129
+ typeof build.version === "string" &&
130
+ !!build.version &&
131
+ build.version !== "unknown" &&
132
+ typeof build.revision === "string" &&
133
+ !!build.revision &&
134
+ build.revision !== "unknown"
135
+ );
59
136
  }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { canonicalizeDiagnosticIdentity } from "../diagnostics/path-identity.ts";
2
3
  import {
3
4
  SUPERVISOR_DIAGNOSTIC_LIMITS,
4
5
  SUPERVISOR_TIMELINE_SCHEMA_VERSION,
@@ -129,5 +130,5 @@ function clusterRelatedSources(
129
130
  }
130
131
 
131
132
  function stableId(prefix: string, value: string): string {
132
- return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 24)}`;
133
+ return `${prefix}_${createHash("sha256").update(canonicalizeDiagnosticIdentity(value)).digest("hex").slice(0, 24)}`;
133
134
  }
@@ -20,7 +20,7 @@ import {
20
20
  import { stateDirMode, stateFileMode } from "../storage/modes.ts";
21
21
  import { readWorkflowApproval } from "../workflow/approvals.ts";
22
22
  import { stableDigest } from "../workflow/durable-record.ts";
23
- import { readWorkflowProof } from "../workflow/proof.ts";
23
+ import { deriveRunFailureClass, readWorkflowProof } from "../workflow/proof.ts";
24
24
  import { readWorkflowRunManifest, workflowScriptDigest } from "../workflow/run-state.ts";
25
25
  import { WORKFLOW_TRANSCRIPT_EVENT_BYTES, workflowTranscriptPath } from "../workflow/transcript.ts";
26
26
  import { isWorkspaceBoundExecutionEvidence } from "../workflow/workspaces/validate.ts";
@@ -718,14 +718,14 @@ function inspectAttempt(
718
718
  proof.acceptance.summary.unknown === 0
719
719
  ? "succeeded"
720
720
  : "failed";
721
- // A failed attempt the run classified as uninformative about the work is
722
- // uncharged (ADR 0046). Absent class ⇒ charged, as before. Read only for a
723
- // failed attempt: a succeeded run never carries a class.
721
+ // Re-derive no-effect eligibility from every participant rather than
722
+ // trusting a persisted transport class. Missing evidence stays charged.
723
+ // The script's explicit decision authority remains a separate path.
724
724
  if (
725
725
  attempt.status === "failed" &&
726
- (proof.run.class === "environment" ||
727
- proof.run.class === "upstream" ||
728
- proof.run.class === "decision")
726
+ (proof.run.class === "decision" ||
727
+ ((proof.run.class === "environment" || proof.run.class === "upstream") &&
728
+ deriveRunFailureClass(proof.run.status, proof.agents) === proof.run.class))
729
729
  ) {
730
730
  attempt.uncharged = proof.run.class;
731
731
  if (proof.run.class === "decision" && typeof proof.run.decision_id === "string") {