harnery 0.43.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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -45,7 +45,16 @@ import { freezeWorkflowAttemptContext } from "./attempt-context.ts";
45
45
  import { type BillingProbe, probeBilling } from "./billing.ts";
46
46
  import { stableDigest } from "./durable-record.ts";
47
47
  import { evidencePreflightError } from "./evidence-preflight.ts";
48
- import { endWorkflowChildSessionV3, startWorkflowChildSessionV3 } from "./live-session-v3.ts";
48
+ import {
49
+ canRetryFailedSpawn,
50
+ observeSpawnFailure,
51
+ resolveFailureEvidence,
52
+ } from "./failure-evidence.ts";
53
+ import {
54
+ endWorkflowChildSessionV3,
55
+ recordWorkflowChildTerminalFailureV3,
56
+ startWorkflowChildSessionV3,
57
+ } from "./live-session-v3.ts";
49
58
  import {
50
59
  buildWorkflowProof,
51
60
  createEvidenceRecord,
@@ -404,7 +413,13 @@ async function executeWorkflow(
404
413
  durationMs,
405
414
  transcriptPath,
406
415
  before: resumeState.manifest.repository_before,
407
- after: resumeState.manifest.repository_before,
416
+ after: {
417
+ cwd: frozenBinding.active_root,
418
+ observation: "unavailable",
419
+ observation_detail:
420
+ "Workspace reattachment failed; no trusted after-run snapshot was observed.",
421
+ dirty_paths: [],
422
+ },
408
423
  agents: [],
409
424
  evidence: [],
410
425
  diagnosticAdmission: initialDiagnosticAdmissionProof(diagnosticAdmissionConfig),
@@ -606,6 +621,7 @@ async function executeWorkflow(
606
621
  let policySeq = 0;
607
622
  const billingProbed = new Map<AdapterName, BillingProbe>();
608
623
  const agentProofs = new Map<string, WorkflowAgentProof>();
624
+ let terminalRecordingError: Error | undefined;
609
625
  const evidenceRecords: WorkflowEvidenceRecord[] = [];
610
626
  const policyDecisions = new Map<string, PolicyDecision>(
611
627
  history?.policyDecisions.map((decision) => [decision.id, decision]),
@@ -1022,6 +1038,49 @@ async function executeWorkflow(
1022
1038
  });
1023
1039
  agentProof.access = access;
1024
1040
 
1041
+ let childGenerationId: string | undefined;
1042
+ let recordAgentEnd: (() => void) | undefined;
1043
+ const finalizeChild = (): void => {
1044
+ try {
1045
+ if (childGenerationId)
1046
+ endWorkflowChildSessionV3({
1047
+ coordRoot: opts.coordRoot,
1048
+ instanceId: childInstanceId(runId, id),
1049
+ runId,
1050
+ agentId: id,
1051
+ adapter,
1052
+ cleanExit: agentProof.status === "succeeded",
1053
+ expectedGenerationId: childGenerationId,
1054
+ });
1055
+ // Result publication is the resume-cache boundary, after canonical custody.
1056
+ recordAgentEnd?.();
1057
+ } catch (error) {
1058
+ let reason = `V3 terminal recording failed: ${(error as Error).message}`;
1059
+ try {
1060
+ if (childGenerationId)
1061
+ recordWorkflowChildTerminalFailureV3({
1062
+ coordRoot: opts.coordRoot,
1063
+ instanceId: childInstanceId(runId, id),
1064
+ runId,
1065
+ agentId: id,
1066
+ adapter,
1067
+ expectedGenerationId: childGenerationId,
1068
+ reason,
1069
+ });
1070
+ } catch (visibilityError) {
1071
+ reason += `; blocked lifecycle recording failed: ${(visibilityError as Error).message}`;
1072
+ }
1073
+ const failure = new Error(agentProof.error ? `${agentProof.error}; ${reason}` : reason, {
1074
+ cause: error,
1075
+ });
1076
+ terminalRecordingError ??= failure;
1077
+ agentProof.status = "failed";
1078
+ agentProof.error = failure.message;
1079
+ transcript("agent.failed", { id, key, error: failure.message });
1080
+ log(`[${name}] ${failure.message}`);
1081
+ throw failure;
1082
+ }
1083
+ };
1025
1084
  await acquire();
1026
1085
  try {
1027
1086
  await ensureDiagnosticAdmissionObserved();
@@ -1043,7 +1102,7 @@ async function executeWorkflow(
1043
1102
  // Headless children do not reliably fire adapter hooks. The engine owns
1044
1103
  // their canonical V3 start so every dispatched child has an auditable
1045
1104
  // generation before it begins work.
1046
- startWorkflowChildSessionV3({
1105
+ const child = startWorkflowChildSessionV3({
1047
1106
  coordRoot: opts.coordRoot,
1048
1107
  instanceId: childInstanceId(runId, id),
1049
1108
  runId,
@@ -1052,10 +1111,13 @@ async function executeWorkflow(
1052
1111
  label,
1053
1112
  model: agentOpts.model,
1054
1113
  });
1114
+ if (!child.v3_generation_id) throw new Error("workflow_child_v3_generation_missing");
1115
+ childGenerationId = child.v3_generation_id;
1055
1116
 
1056
1117
  let attemptPrompt = dispatchPrompt;
1057
1118
  let last: SpawnResult | null = null;
1058
1119
  let agentCostUsd = 0;
1120
+ let informativeAttempt: ReturnType<typeof observeSpawnFailure> | undefined;
1059
1121
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1060
1122
  last = await spawner({
1061
1123
  prompt: attemptPrompt,
@@ -1078,33 +1140,51 @@ async function executeWorkflow(
1078
1140
  costUsd += last.costUsd ?? 0;
1079
1141
 
1080
1142
  if (!last.ok) {
1081
- transcript("agent.attempt_failed", { id, attempt, error: last.error });
1082
- // ADR 0046: an environment failure (the binary was absent) cannot be
1083
- // helped by retrying an unchanged environment, so stop the in-agent
1084
- // retry too — not just the outer attempt/replan budget. An upstream
1085
- // refusal keeps retrying here: the vendor may recover mid-loop.
1086
- if (last.class === "environment") break;
1087
- continue; // spawn-level failure: retry with the original prompt
1143
+ const observed =
1144
+ last.failureEvidence === undefined
1145
+ ? observeSpawnFailure({}, last.text, "final_message")
1146
+ : resolveFailureEvidence(last.failureEvidence);
1147
+ agentProof.failure_evidence = informativeAttempt
1148
+ ? {
1149
+ ...observed,
1150
+ effect: { state: "unknown" },
1151
+ partial_output: informativeAttempt.partial_output,
1152
+ }
1153
+ : observed;
1154
+ transcript("agent.attempt_failed", {
1155
+ id,
1156
+ attempt,
1157
+ error: last.error,
1158
+ failure_evidence: observed,
1159
+ result_digest: digestResult(last.text, "text"),
1160
+ text: last.text,
1161
+ });
1162
+ if (!canRetryFailedSpawn({ ...last, failureEvidence: agentProof.failure_evidence }))
1163
+ break;
1164
+ continue;
1088
1165
  }
1166
+ informativeAttempt = observeSpawnFailure({}, last.text, "final_message");
1089
1167
  if (!agentOpts.schema) {
1090
1168
  agentProof.status = "succeeded";
1091
1169
  agentProof.cost_usd =
1092
1170
  agentCostUsd > 0 || last.costUsd !== undefined ? agentCostUsd : undefined;
1093
1171
  agentProof.session_id = last.sessionId;
1094
1172
  agentProof.result = digestResult(last.text, "text");
1095
- transcriptAgentEnd(
1096
- {
1097
- id,
1098
- key,
1099
- attempts: attempt,
1100
- cost_usd: last.costUsd,
1101
- total_cost_usd: agentCostUsd,
1102
- duration_ms: last.durationMs,
1103
- session_id: last.sessionId,
1104
- },
1105
- "text",
1106
- last.text,
1107
- );
1173
+ const completed = last;
1174
+ recordAgentEnd = () =>
1175
+ transcriptAgentEnd(
1176
+ {
1177
+ id,
1178
+ key,
1179
+ attempts: attempt,
1180
+ cost_usd: completed.costUsd,
1181
+ total_cost_usd: agentCostUsd,
1182
+ duration_ms: completed.durationMs,
1183
+ session_id: completed.sessionId,
1184
+ },
1185
+ "text",
1186
+ completed.text,
1187
+ );
1108
1188
  return last.text;
1109
1189
  }
1110
1190
 
@@ -1119,23 +1199,32 @@ async function executeWorkflow(
1119
1199
  agentCostUsd > 0 || last.costUsd !== undefined ? agentCostUsd : undefined;
1120
1200
  agentProof.session_id = last.sessionId;
1121
1201
  agentProof.result = digestResult(parsed.value, "json");
1122
- transcriptAgentEnd(
1123
- {
1124
- id,
1125
- key,
1126
- attempts: attempt,
1127
- cost_usd: last.costUsd,
1128
- total_cost_usd: agentCostUsd,
1129
- duration_ms: last.durationMs,
1130
- session_id: last.sessionId,
1131
- },
1132
- "json",
1133
- parsed.value,
1134
- );
1202
+ const completed = last;
1203
+ recordAgentEnd = () =>
1204
+ transcriptAgentEnd(
1205
+ {
1206
+ id,
1207
+ key,
1208
+ attempts: attempt,
1209
+ cost_usd: completed.costUsd,
1210
+ total_cost_usd: agentCostUsd,
1211
+ duration_ms: completed.durationMs,
1212
+ session_id: completed.sessionId,
1213
+ },
1214
+ "json",
1215
+ parsed.value,
1216
+ );
1135
1217
  return parsed.value;
1136
1218
  }
1137
1219
 
1138
- transcript("agent.schema_retry", { id, attempt, problems });
1220
+ agentProof.failure_evidence = informativeAttempt;
1221
+ transcript("agent.schema_retry", {
1222
+ id,
1223
+ attempt,
1224
+ problems,
1225
+ rejected_text: last.text,
1226
+ result_digest: digestResult(last.text, "text"),
1227
+ });
1139
1228
  // Keep the original cache identity while making the actual output
1140
1229
  // contract explicit. A full bounded copy of the rejected value lets the
1141
1230
  // child repair omissions instead of reconstructing an unseen object.
@@ -1168,20 +1257,12 @@ async function executeWorkflow(
1168
1257
  throw error;
1169
1258
  } finally {
1170
1259
  reservedCostUsd = Math.max(0, reservedCostUsd - reservedForDispatch);
1171
- // Record the canonical terminal before removing the disposable cache.
1172
1260
  try {
1173
- endWorkflowChildSessionV3({
1174
- coordRoot: opts.coordRoot,
1175
- instanceId: childInstanceId(runId, id),
1176
- runId,
1177
- agentId: id,
1178
- adapter,
1179
- cleanExit: agentProof.status === "succeeded",
1180
- });
1181
- } catch (error) {
1182
- log(`[${name}] V3 terminal recording failed: ${(error as Error).message}`);
1261
+ finalizeChild();
1262
+ } finally {
1263
+ // A slot is disposable; unresolved custody remains visible and failed.
1264
+ release();
1183
1265
  }
1184
- release();
1185
1266
  }
1186
1267
  };
1187
1268
 
@@ -1286,6 +1367,7 @@ async function executeWorkflow(
1286
1367
  if (initializationError) throw initializationError;
1287
1368
  if (!workflow) throw new Error("workflow module initialization did not produce an executable");
1288
1369
  const result = await workflow(ctx);
1370
+ if (terminalRecordingError) throw terminalRecordingError;
1289
1371
  const endedAt = new Date().toISOString();
1290
1372
  const durationMs = Date.now() - t0;
1291
1373
  const workspaceAttestation = workspaceBinding
@@ -0,0 +1,141 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { AdapterRawResult } from "../adapters/types.ts";
3
+ import type { SpawnFailureEvidence, SpawnOutputSource, SpawnResult } from "./types.ts";
4
+
5
+ export function unknownFailureEvidence(): SpawnFailureEvidence {
6
+ return {
7
+ effect: { state: "unknown" },
8
+ refusal: { state: "unknown" },
9
+ partial_output: { state: "unknown" },
10
+ };
11
+ }
12
+
13
+ /** Built-in subprocess adapters observe output and process-start failures.
14
+ * Their current vendor envelopes do not prove effects or native refusal.
15
+ * In particular, blank output and 429/5xx statuses leave both unknown. */
16
+ export function observeSpawnFailure(
17
+ raw: Pick<AdapterRawResult, "spawnErrno" | "timedOut"> &
18
+ Partial<Pick<AdapterRawResult, "stdout" | "resultFileText">>,
19
+ text: string,
20
+ source: SpawnOutputSource,
21
+ ): SpawnFailureEvidence {
22
+ const bytes = Buffer.byteLength(text);
23
+ return {
24
+ effect:
25
+ raw.spawnErrno === "ENOENT" &&
26
+ !raw.timedOut &&
27
+ bytes === 0 &&
28
+ !raw.stdout?.trim() &&
29
+ !raw.resultFileText?.trim()
30
+ ? { state: "none", source: "spawn_errno", reference: "ENOENT" }
31
+ : { state: "unknown" },
32
+ refusal: { state: "unknown" },
33
+ partial_output:
34
+ bytes > 0
35
+ ? {
36
+ state: "present",
37
+ source,
38
+ bytes,
39
+ sha256: createHash("sha256").update(text).digest("hex"),
40
+ }
41
+ : { state: "absent", source },
42
+ };
43
+ }
44
+
45
+ function record(value: unknown): value is Record<string, unknown> {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+
49
+ function keys(value: Record<string, unknown>, expected: readonly string[]): boolean {
50
+ return (
51
+ Object.keys(value).length === expected.length &&
52
+ expected.every((key) => Object.hasOwn(value, key))
53
+ );
54
+ }
55
+
56
+ function reference(value: unknown): value is string {
57
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 512;
58
+ }
59
+
60
+ function validEffect(value: unknown): boolean {
61
+ if (!record(value)) return false;
62
+ if (value.state === "unknown") return keys(value, ["state"]);
63
+ if (!keys(value, ["state", "source", "reference"])) return false;
64
+ if (value.state === "none" && value.source === "spawn_errno") {
65
+ return value.reference === "ENOENT";
66
+ }
67
+ return (
68
+ (value.state === "none" || value.state === "observed") &&
69
+ value.source === "adapter_evidence" &&
70
+ reference(value.reference)
71
+ );
72
+ }
73
+
74
+ function validRefusal(value: unknown): boolean {
75
+ if (!record(value)) return false;
76
+ if (value.state === "unknown") return keys(value, ["state"]);
77
+ return (
78
+ keys(value, ["state", "source", "reference"]) &&
79
+ value.state === "refused" &&
80
+ value.source === "vendor_envelope" &&
81
+ reference(value.reference)
82
+ );
83
+ }
84
+
85
+ function validOutput(value: unknown): boolean {
86
+ if (!record(value)) return false;
87
+ if (value.state === "unknown") return keys(value, ["state"]);
88
+ if (
89
+ value.source !== "final_message" &&
90
+ value.source !== "vendor_envelope" &&
91
+ value.source !== "stdout"
92
+ ) {
93
+ return false;
94
+ }
95
+ if (value.state === "absent") return keys(value, ["state", "source"]);
96
+ return (
97
+ value.state === "present" &&
98
+ keys(value, ["state", "source", "bytes", "sha256"]) &&
99
+ Number.isSafeInteger(value.bytes) &&
100
+ (value.bytes as number) > 0 &&
101
+ typeof value.sha256 === "string" &&
102
+ /^[a-f0-9]{64}$/.test(value.sha256)
103
+ );
104
+ }
105
+
106
+ export function isSpawnFailureEvidence(value: unknown): value is SpawnFailureEvidence {
107
+ return (
108
+ record(value) &&
109
+ keys(value, ["effect", "refusal", "partial_output"]) &&
110
+ validEffect(value.effect) &&
111
+ validRefusal(value.refusal) &&
112
+ validOutput(value.partial_output)
113
+ );
114
+ }
115
+
116
+ /** Missing observations are unknown; malformed positive claims are rejected.
117
+ * This is also used when reading persisted workflow proof. */
118
+ export function resolveFailureEvidence(value: unknown): SpawnFailureEvidence {
119
+ if (value === undefined) return unknownFailureEvidence();
120
+ if (!isSpawnFailureEvidence(value)) throw new Error("invalid spawn failure evidence");
121
+ return value;
122
+ }
123
+
124
+ export function failureEvidenceAllowsUncharged(value: unknown): boolean {
125
+ const evidence = resolveFailureEvidence(value);
126
+ return (
127
+ evidence.effect.state === "none" &&
128
+ evidence.refusal.state !== "refused" &&
129
+ evidence.partial_output.state === "absent"
130
+ );
131
+ }
132
+
133
+ /** A provider retry requires positive no-effect evidence. A missing binary is
134
+ * known not to have started, but retrying that unchanged environment cannot help. */
135
+ export function canRetryFailedSpawn(result: SpawnResult): boolean {
136
+ return (
137
+ !result.ok &&
138
+ result.class !== "environment" &&
139
+ failureEvidenceAllowsUncharged(result.failureEvidence)
140
+ );
141
+ }
@@ -166,7 +166,6 @@ export {
166
166
  isWorkspaceLifecycleState,
167
167
  listWorkflowWorkspaceInspections,
168
168
  prepareIntegration,
169
- prepareReclaim,
170
169
  probeLocalGitWorktreeProvider,
171
170
  readWorkflowWorkspaceStatus,
172
171
  renderWorkflowWorkspaceStatus,
@@ -1,9 +1,16 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, linkSync, readdirSync, readFileSync, renameSync, unlinkSync } from "node:fs";
3
+ import { basename, dirname } from "node:path";
1
4
  import { type Adapter, adapterFromPlatform } from "../adapter.ts";
2
- import { recordLiveTaskChangeV3 } from "../agents/live-authority-v3.ts";
5
+ import {
6
+ recordLiveLifecycleChangeV3,
7
+ recordLiveTaskChangeV3,
8
+ } from "../agents/live-authority-v3.ts";
3
9
  import { endSessionExplicitV3 } from "../agents/session-finalizer-v3.ts";
10
+ import { heartbeatPath } from "../agents/state/heartbeat-reader.ts";
4
11
  import {
5
- clearCoordinationCache,
6
12
  type Heartbeat,
13
+ readHeartbeat,
7
14
  setIdentityCache,
8
15
  } from "../agents/state/heartbeat-writer.ts";
9
16
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
@@ -12,9 +19,17 @@ import { assignName } from "../agents/state/names.ts";
12
19
  import { coordFreshnessSeconds } from "../config.ts";
13
20
  import { ensureEventLedgerV3 } from "../events/v3/bootstrap.ts";
14
21
  import {
22
+ readCoordinationViewV3,
23
+ requireAuthoritySafeCoordinationViewV3,
24
+ } from "../events/v3/coordination-view.ts";
25
+ import { withGenerationCustodyV3 } from "../events/v3/generation-custody.ts";
26
+ import {
27
+ liveInstanceIdV3,
15
28
  recordLiveHookSignalV3,
16
29
  resolveLiveEventLedgerRouteV3,
17
30
  } from "../events/v3/live-routing.ts";
31
+ import { readHookProducerStateV3 } from "../events/v3/producers/recorder.ts";
32
+ import { fsyncParentDirectory } from "./durable-record.ts";
18
33
  import { stableScopeId } from "./scope-id.ts";
19
34
 
20
35
  export interface WorkflowChildSessionV3Input {
@@ -77,16 +92,45 @@ export function startWorkflowChildSessionV3(input: WorkflowChildSessionV3Input):
77
92
  }
78
93
  const projected = readLiveCoordinationRow(input.coordRoot, input.instanceId);
79
94
  if (!projected) throw new Error("workflow_child_v3_projection_missing");
95
+ const own = readHookProducerStateV3(input.coordRoot, adapter, sessionId);
96
+ if (!own || own.terminal || own.generation_id !== projected.v3_generation_id) {
97
+ throw new Error("workflow_child_v3_start_generation_changed");
98
+ }
80
99
  return projected;
81
100
  }
82
101
 
83
102
  /** Record the authoritative terminal before removing the disposable coordination cache. */
84
103
  export function endWorkflowChildSessionV3(
85
- input: WorkflowChildSessionV3Input & { cleanExit: boolean },
104
+ input: WorkflowChildSessionV3Input & { cleanExit: boolean; expectedGenerationId: string },
105
+ ): void {
106
+ withGenerationCustodyV3(input.coordRoot, [liveInstanceIdV3(input.instanceId)], () =>
107
+ endExpectedChild(input),
108
+ );
109
+ }
110
+
111
+ function endExpectedChild(
112
+ input: WorkflowChildSessionV3Input & { cleanExit: boolean; expectedGenerationId: string },
86
113
  ): void {
87
114
  requireV3Route(input.coordRoot);
115
+ const view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(input.coordRoot));
116
+ const instanceId = liveInstanceIdV3(input.instanceId);
117
+ const current = view.instances[instanceId];
118
+ const terminal = view.terminal_generations[input.expectedGenerationId];
119
+ if (terminal?.instance_id === instanceId) recoverHeldCaches(input);
120
+ if (current && current.generation_id !== input.expectedGenerationId) {
121
+ throw new Error("workflow_child_v3_generation_changed");
122
+ }
123
+ if (!current) {
124
+ if (!terminal || terminal.instance_id !== instanceId)
125
+ throw new Error("workflow_child_v3_generation_missing");
126
+ if (terminal.terminal?.outcome !== (input.cleanExit ? "succeeded" : "failed")) {
127
+ throw new Error("workflow_child_v3_terminal_outcome_changed");
128
+ }
129
+ clearExpectedCache(input);
130
+ return;
131
+ }
88
132
  const row = readLiveCoordinationRow(input.coordRoot, input.instanceId);
89
- if (!row?.v3_instance_id || !row.v3_generation_id) {
133
+ if (!row?.v3_instance_id || row.v3_generation_id !== input.expectedGenerationId) {
90
134
  throw new Error("workflow_child_v3_generation_missing");
91
135
  }
92
136
  const result = endSessionExplicitV3({
@@ -101,7 +145,104 @@ export function endWorkflowChildSessionV3(
101
145
  `workflow_child_v3_end_failed:${result.state}${"reason" in result ? `:${result.reason}` : ""}`,
102
146
  );
103
147
  }
104
- clearCoordinationCache(input.coordRoot, input.instanceId);
148
+ clearExpectedCache(input);
149
+ }
150
+
151
+ /** Persist unresolved terminal custody only for the generation this dispatch owns. */
152
+ export function recordWorkflowChildTerminalFailureV3(
153
+ input: WorkflowChildSessionV3Input & { expectedGenerationId: string; reason: string },
154
+ ): void {
155
+ withGenerationCustodyV3(input.coordRoot, [liveInstanceIdV3(input.instanceId)], () => {
156
+ const row = readLiveCoordinationRow(input.coordRoot, input.instanceId);
157
+ if (!row || row.v3_generation_id !== input.expectedGenerationId) return;
158
+ recordLiveLifecycleChangeV3({
159
+ coordRoot: input.coordRoot,
160
+ owner: input.instanceId,
161
+ nativeSessionId: input.sessionId ?? input.instanceId,
162
+ adapter: workflowAdapter(input.adapter),
163
+ expectedGenerationId: input.expectedGenerationId as `gen_${string}`,
164
+ state: "blocked",
165
+ reason: input.reason,
166
+ });
167
+ });
168
+ }
169
+
170
+ function heldPrefix(input: WorkflowChildSessionV3Input & { expectedGenerationId: string }): string {
171
+ if (!/^gen_[a-zA-Z0-9._-]{1,128}$/.test(input.expectedGenerationId))
172
+ throw new Error("workflow_child_v3_generation_invalid");
173
+ return `${basename(heartbeatPath(input.coordRoot, input.instanceId))}.terminal-${input.expectedGenerationId}-`;
174
+ }
175
+
176
+ function recoverHeldCaches(
177
+ input: WorkflowChildSessionV3Input & { expectedGenerationId: string },
178
+ ): void {
179
+ const path = heartbeatPath(input.coordRoot, input.instanceId);
180
+ if (!existsSync(dirname(path))) return;
181
+ const prefix = heldPrefix(input);
182
+ for (const name of readdirSync(dirname(path)).filter((value) => value.startsWith(prefix))) {
183
+ finishHeldCache(`${dirname(path)}/${name}`, path, input.expectedGenerationId);
184
+ }
185
+ }
186
+
187
+ function clearExpectedCache(
188
+ input: WorkflowChildSessionV3Input & { expectedGenerationId: string },
189
+ ): void {
190
+ recoverHeldCaches(input);
191
+ if (
192
+ readHeartbeat(input.coordRoot, input.instanceId)?.v3_generation_id !==
193
+ input.expectedGenerationId
194
+ )
195
+ return;
196
+ const path = heartbeatPath(input.coordRoot, input.instanceId);
197
+ const held = `${dirname(path)}/${heldPrefix(input)}${randomUUID()}`;
198
+ try {
199
+ // A unique held name pins the actual bytes atomically. Checking before
200
+ // unlink alone could consume a replacement published at this boundary.
201
+ renameSync(path, held);
202
+ fsyncParentDirectory(path);
203
+ } catch (error) {
204
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
205
+ throw error;
206
+ }
207
+ finishHeldCache(held, path, input.expectedGenerationId);
208
+ }
209
+
210
+ function finishHeldCache(held: string, path: string, expectedGenerationId: string): void {
211
+ let removed = false;
212
+ let cleanupFailed = false;
213
+ let cleanupError: unknown;
214
+ try {
215
+ const cache = JSON.parse(readFileSync(held, "utf8")) as Heartbeat;
216
+ if (cache.v3_generation_id === expectedGenerationId) {
217
+ unlinkSync(held);
218
+ fsyncParentDirectory(held);
219
+ removed = true;
220
+ }
221
+ } catch (error) {
222
+ cleanupFailed = true;
223
+ cleanupError = error;
224
+ }
225
+ if (!removed) {
226
+ try {
227
+ // Never overwrite a later publication. Keep the held evidence when
228
+ // another cache already owns the live name.
229
+ linkSync(held, path);
230
+ fsyncParentDirectory(path);
231
+ unlinkSync(held);
232
+ fsyncParentDirectory(held);
233
+ } catch (error) {
234
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
235
+ if (cleanupFailed) {
236
+ throw new AggregateError(
237
+ [cleanupError, error],
238
+ `workflow_child_v3_cache_cleanup_and_restore_failed: ${String(cleanupError)}; restoration: ${String(error)}`,
239
+ );
240
+ }
241
+ throw error;
242
+ }
243
+ }
244
+ }
245
+ if (cleanupFailed) throw cleanupError;
105
246
  }
106
247
 
107
248
  function requireV3Route(coordRoot: string) {