harnery 0.45.0 → 0.45.2

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 (235) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +6 -45
  12. package/dist/core/agents/coord-client.d.ts.map +1 -1
  13. package/dist/core/agents/coord-client.js +0 -18
  14. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  15. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  16. package/dist/core/agents/live-authority-v3.js +0 -11
  17. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  18. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  19. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  20. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  21. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  22. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  23. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  24. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  25. package/dist/core/agents/state/pidmap.js +2 -7
  26. package/dist/core/agents/state/proc-start.d.ts +20 -31
  27. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  28. package/dist/core/agents/state/proc-start.js +37 -70
  29. package/dist/core/agents/state/service-process.d.ts +61 -0
  30. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  31. package/dist/core/agents/state/service-process.js +143 -0
  32. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  33. package/dist/core/agents/state/stale-sweep.js +70 -180
  34. package/dist/core/artifacts/activity.d.ts +4 -1
  35. package/dist/core/artifacts/activity.d.ts.map +1 -1
  36. package/dist/core/artifacts/activity.js +8 -2
  37. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  38. package/dist/core/artifacts/delivery-card.js +1 -2
  39. package/dist/core/artifacts/index.d.ts.map +1 -1
  40. package/dist/core/artifacts/index.js +38 -12
  41. package/dist/core/context/index.d.ts +6 -4
  42. package/dist/core/context/index.d.ts.map +1 -1
  43. package/dist/core/context/index.js +194 -256
  44. package/dist/core/context/observation.d.ts +9 -9
  45. package/dist/core/context/observation.d.ts.map +1 -1
  46. package/dist/core/context/observation.js +37 -6
  47. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  48. package/dist/core/events/v3/authority-outbox.js +0 -29
  49. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  50. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  51. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +64 -77
  54. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  55. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  56. package/dist/core/events/v3/runtime-build.js +44 -0
  57. package/dist/core/governor/index.d.ts +2 -1
  58. package/dist/core/governor/index.d.ts.map +1 -1
  59. package/dist/core/governor/index.js +2 -1
  60. package/dist/core/governor/service-read.d.ts +3 -10
  61. package/dist/core/governor/service-read.d.ts.map +1 -1
  62. package/dist/core/governor/service-read.js +10 -94
  63. package/dist/core/governor/service.d.ts +3 -7
  64. package/dist/core/governor/service.d.ts.map +1 -1
  65. package/dist/core/governor/service.js +17 -182
  66. package/dist/core/hooks/cli.js +78 -48
  67. package/dist/core/hooks/resolve/owner.js +2 -2
  68. package/dist/core/resources/sampler.js +2 -2
  69. package/dist/core/resources/service-status.d.ts +2 -14
  70. package/dist/core/resources/service-status.d.ts.map +1 -1
  71. package/dist/core/resources/service-status.js +3 -23
  72. package/dist/core/resources/service.d.ts +3 -7
  73. package/dist/core/resources/service.d.ts.map +1 -1
  74. package/dist/core/resources/service.js +12 -101
  75. package/dist/core/resources/status.d.ts.map +1 -1
  76. package/dist/core/resources/status.js +3 -2
  77. package/dist/core/semantic/service.d.ts.map +1 -1
  78. package/dist/core/semantic/service.js +9 -2
  79. package/dist/core/servers/index.d.ts +11 -10
  80. package/dist/core/servers/index.d.ts.map +1 -1
  81. package/dist/core/servers/index.js +82 -192
  82. package/dist/core/servers/tunnels.d.ts +3 -5
  83. package/dist/core/servers/tunnels.d.ts.map +1 -1
  84. package/dist/core/servers/tunnels.js +2 -45
  85. package/dist/core/storage/durable-history.js +56 -22
  86. package/dist/core/storage/segments.d.ts.map +1 -1
  87. package/dist/core/storage/segments.js +73 -17
  88. package/dist/core/supervisor/service.d.ts +4 -7
  89. package/dist/core/supervisor/service.d.ts.map +1 -1
  90. package/dist/core/supervisor/service.js +23 -122
  91. package/dist/core/supervisor/services.js +2 -2
  92. package/dist/core/supervisor/status.d.ts +3 -17
  93. package/dist/core/supervisor/status.d.ts.map +1 -1
  94. package/dist/core/supervisor/status.js +7 -70
  95. package/dist/core/work/state.js +7 -7
  96. package/dist/core/workflow/engine.d.ts.map +1 -1
  97. package/dist/core/workflow/engine.js +23 -66
  98. package/dist/core/workflow/index.d.ts +3 -4
  99. package/dist/core/workflow/index.d.ts.map +1 -1
  100. package/dist/core/workflow/index.js +2 -2
  101. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  102. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  103. package/dist/core/workflow/live-session-v3.js +5 -126
  104. package/dist/core/workflow/proof.d.ts +4 -4
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +34 -86
  107. package/dist/core/workflow/resume-lock.d.ts +17 -0
  108. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  109. package/dist/core/workflow/resume-lock.js +158 -0
  110. package/dist/core/workflow/run-state.d.ts +0 -21
  111. package/dist/core/workflow/run-state.d.ts.map +1 -1
  112. package/dist/core/workflow/run-state.js +3 -174
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +0 -19
  115. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  116. package/dist/core/workflow/spawn-codex.js +0 -13
  117. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-cursor.js +0 -19
  119. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-opencode.js +0 -14
  121. package/dist/core/workflow/types.d.ts +11 -54
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  124. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  125. package/dist/core/workflow/workspaces/leases.js +1 -1
  126. package/dist/core/workflow/workspaces/local-git.js +91 -210
  127. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  128. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  129. package/dist/core/workflow/workspaces/ownership.js +3 -130
  130. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  131. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  132. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  133. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  134. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  135. package/dist/core/workflow/workspaces/recovery.js +1 -1
  136. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/state.js +1 -4
  138. package/dist/lib/identities/assume.d.ts.map +1 -1
  139. package/dist/lib/identities/assume.js +0 -4
  140. package/dist/lib/tunnel/state.d.ts +6 -13
  141. package/dist/lib/tunnel/state.d.ts.map +1 -1
  142. package/dist/lib/tunnel/state.js +12 -58
  143. package/package.json +1 -6
  144. package/src/commander.ts +0 -10
  145. package/src/commands/files.ts +5 -13
  146. package/src/commands/servers.ts +23 -12
  147. package/src/commands/tunnel.ts +166 -533
  148. package/src/commands/workflow.ts +8 -58
  149. package/src/core/agents/coord-client.ts +0 -20
  150. package/src/core/agents/live-authority-v3.ts +0 -17
  151. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  152. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  153. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  154. package/src/core/agents/state/pidmap.ts +2 -15
  155. package/src/core/agents/state/proc-start.ts +41 -69
  156. package/src/core/agents/state/service-process.ts +217 -0
  157. package/src/core/agents/state/stale-sweep.ts +82 -247
  158. package/src/core/artifacts/activity.ts +9 -2
  159. package/src/core/artifacts/delivery-card.ts +1 -7
  160. package/src/core/artifacts/index.ts +45 -12
  161. package/src/core/context/index.ts +207 -272
  162. package/src/core/context/observation.ts +40 -9
  163. package/src/core/events/v3/authority-outbox.ts +0 -56
  164. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  165. package/src/core/events/v3/producers/recorder.ts +68 -94
  166. package/src/core/events/v3/runtime-build.ts +62 -0
  167. package/src/core/governor/index.ts +1 -2
  168. package/src/core/governor/service-read.ts +15 -114
  169. package/src/core/governor/service.ts +36 -220
  170. package/src/core/hooks/cli.ts +85 -47
  171. package/src/core/hooks/resolve/owner.ts +2 -2
  172. package/src/core/resources/sampler.ts +3 -2
  173. package/src/core/resources/service-status.ts +6 -35
  174. package/src/core/resources/service.ts +23 -133
  175. package/src/core/resources/status.ts +3 -2
  176. package/src/core/semantic/service.ts +8 -2
  177. package/src/core/servers/index.ts +86 -228
  178. package/src/core/servers/tunnels.ts +3 -55
  179. package/src/core/storage/durable-history.ts +52 -20
  180. package/src/core/storage/segments.ts +79 -17
  181. package/src/core/supervisor/service.ts +33 -155
  182. package/src/core/supervisor/services.ts +2 -2
  183. package/src/core/supervisor/status.ts +10 -85
  184. package/src/core/work/state.ts +7 -7
  185. package/src/core/workflow/engine.ts +22 -74
  186. package/src/core/workflow/index.ts +2 -13
  187. package/src/core/workflow/live-session-v3.ts +5 -146
  188. package/src/core/workflow/proof.ts +40 -104
  189. package/src/core/workflow/resume-lock.ts +179 -0
  190. package/src/core/workflow/run-state.ts +3 -207
  191. package/src/core/workflow/spawn-claude.ts +0 -18
  192. package/src/core/workflow/spawn-codex.ts +0 -13
  193. package/src/core/workflow/spawn-cursor.ts +0 -18
  194. package/src/core/workflow/spawn-opencode.ts +0 -14
  195. package/src/core/workflow/types.ts +11 -41
  196. package/src/core/workflow/workspaces/leases.ts +1 -1
  197. package/src/core/workflow/workspaces/local-git.ts +120 -239
  198. package/src/core/workflow/workspaces/ownership.ts +3 -158
  199. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  200. package/src/core/workflow/workspaces/recovery.ts +1 -1
  201. package/src/core/workflow/workspaces/state.ts +1 -5
  202. package/src/lib/identities/assume.ts +0 -4
  203. package/src/lib/tunnel/state.ts +15 -70
  204. package/dist/commands/turn-rollback.d.ts +0 -5
  205. package/dist/commands/turn-rollback.d.ts.map +0 -1
  206. package/dist/commands/turn-rollback.js +0 -76
  207. package/dist/core/agents/state/process-handle.d.ts +0 -26
  208. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  209. package/dist/core/agents/state/process-handle.js +0 -121
  210. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  211. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  212. package/dist/core/agents/state/unchanged-file.js +0 -390
  213. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  214. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  215. package/dist/core/events/v3/generation-custody.js +0 -50
  216. package/dist/core/turn-rollback/content.d.ts +0 -53
  217. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  218. package/dist/core/turn-rollback/content.js +0 -396
  219. package/dist/core/turn-rollback/index.d.ts +0 -72
  220. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  221. package/dist/core/turn-rollback/index.js +0 -443
  222. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  223. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  224. package/dist/core/workflow/failure-evidence.js +0 -114
  225. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  226. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  227. package/dist/core/workflow/recovery-audit.js +0 -116
  228. package/src/commands/turn-rollback.ts +0 -111
  229. package/src/core/agents/state/process-handle.ts +0 -165
  230. package/src/core/agents/state/unchanged-file.ts +0 -456
  231. package/src/core/events/v3/generation-custody.ts +0 -59
  232. package/src/core/turn-rollback/content.ts +0 -499
  233. package/src/core/turn-rollback/index.ts +0 -580
  234. package/src/core/workflow/failure-evidence.ts +0 -141
  235. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -45,16 +45,7 @@ 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 {
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";
48
+ import { endWorkflowChildSessionV3, startWorkflowChildSessionV3 } from "./live-session-v3.ts";
58
49
  import {
59
50
  buildWorkflowProof,
60
51
  createEvidenceRecord,
@@ -62,8 +53,8 @@ import {
62
53
  normalizeWorkflowMeta,
63
54
  writeWorkflowProof,
64
55
  } from "./proof.ts";
56
+ import { acquireWorkflowResumeLease } from "./resume-lock.ts";
65
57
  import {
66
- acquireWorkflowResumeLease,
67
58
  assertWorkflowRunResumable,
68
59
  assertWorkflowScriptUnchanged,
69
60
  WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION,
@@ -1038,38 +1029,22 @@ async function executeWorkflow(
1038
1029
  });
1039
1030
  agentProof.access = access;
1040
1031
 
1041
- let childGenerationId: string | undefined;
1042
1032
  let recordAgentEnd: (() => void) | undefined;
1043
1033
  const finalizeChild = (): void => {
1044
1034
  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.
1035
+ // Record the canonical terminal first. agent.end is the resume-cache
1036
+ // boundary, so a child whose end was not recorded never reports success.
1037
+ endWorkflowChildSessionV3({
1038
+ coordRoot: opts.coordRoot,
1039
+ instanceId: childInstanceId(runId, id),
1040
+ runId,
1041
+ agentId: id,
1042
+ adapter,
1043
+ cleanExit: agentProof.status === "succeeded",
1044
+ });
1056
1045
  recordAgentEnd?.();
1057
1046
  } 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
- }
1047
+ const reason = `V3 terminal recording failed: ${(error as Error).message}`;
1073
1048
  const failure = new Error(agentProof.error ? `${agentProof.error}; ${reason}` : reason, {
1074
1049
  cause: error,
1075
1050
  });
@@ -1102,7 +1077,7 @@ async function executeWorkflow(
1102
1077
  // Headless children do not reliably fire adapter hooks. The engine owns
1103
1078
  // their canonical V3 start so every dispatched child has an auditable
1104
1079
  // generation before it begins work.
1105
- const child = startWorkflowChildSessionV3({
1080
+ startWorkflowChildSessionV3({
1106
1081
  coordRoot: opts.coordRoot,
1107
1082
  instanceId: childInstanceId(runId, id),
1108
1083
  runId,
@@ -1111,13 +1086,10 @@ async function executeWorkflow(
1111
1086
  label,
1112
1087
  model: agentOpts.model,
1113
1088
  });
1114
- if (!child.v3_generation_id) throw new Error("workflow_child_v3_generation_missing");
1115
- childGenerationId = child.v3_generation_id;
1116
1089
 
1117
1090
  let attemptPrompt = dispatchPrompt;
1118
1091
  let last: SpawnResult | null = null;
1119
1092
  let agentCostUsd = 0;
1120
- let informativeAttempt: ReturnType<typeof observeSpawnFailure> | undefined;
1121
1093
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1122
1094
  last = await spawner({
1123
1095
  prompt: attemptPrompt,
@@ -1140,30 +1112,14 @@ async function executeWorkflow(
1140
1112
  costUsd += last.costUsd ?? 0;
1141
1113
 
1142
1114
  if (!last.ok) {
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;
1115
+ transcript("agent.attempt_failed", { id, attempt, error: last.error });
1116
+ // ADR 0046: an environment failure (the binary was absent) cannot be
1117
+ // helped by retrying an unchanged environment, so stop the in-agent
1118
+ // retry too — not just the outer attempt/replan budget. An upstream
1119
+ // refusal keeps retrying here: the vendor may recover mid-loop.
1120
+ if (last.class === "environment") break;
1121
+ continue; // spawn-level failure: retry with the original prompt
1165
1122
  }
1166
- informativeAttempt = observeSpawnFailure({}, last.text, "final_message");
1167
1123
  if (!agentOpts.schema) {
1168
1124
  agentProof.status = "succeeded";
1169
1125
  agentProof.cost_usd =
@@ -1217,14 +1173,7 @@ async function executeWorkflow(
1217
1173
  return parsed.value;
1218
1174
  }
1219
1175
 
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
- });
1176
+ transcript("agent.schema_retry", { id, attempt, problems });
1228
1177
  // Keep the original cache identity while making the actual output
1229
1178
  // contract explicit. A full bounded copy of the rejected value lets the
1230
1179
  // child repair omissions instead of reconstructing an unseen object.
@@ -1260,7 +1209,6 @@ async function executeWorkflow(
1260
1209
  try {
1261
1210
  finalizeChild();
1262
1211
  } finally {
1263
- // A slot is disposable; unresolved custody remains visible and failed.
1264
1212
  release();
1265
1213
  }
1266
1214
  }
@@ -39,23 +39,12 @@ export {
39
39
  rollupAcceptance,
40
40
  writeWorkflowProof,
41
41
  } from "./proof.ts";
42
- export type {
43
- WorkflowRecoveryAuthorization,
44
- WorkflowRecoveryIntent,
45
- WorkflowRecoveryRecord,
46
- } from "./recovery-audit.ts";
47
- export { beginWorkflowRecovery, readWorkflowRecoveryLedger } from "./recovery-audit.ts";
48
- export type {
49
- WorkflowResumeRecoveryOptions,
50
- WorkflowResumeRecoveryResult,
51
- WorkflowRunManifest,
52
- } from "./run-state.ts";
42
+ export { acquireWorkflowResumeLease } from "./resume-lock.ts";
43
+ export type { WorkflowRunManifest } from "./run-state.ts";
53
44
  export {
54
- acquireWorkflowResumeLease,
55
45
  assertWorkflowRunResumable,
56
46
  assertWorkflowScriptUnchanged,
57
47
  readWorkflowRunManifest,
58
- recoverWorkflowResumeLease,
59
48
  WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION,
60
49
  workflowRunManifestPath,
61
50
  workflowScriptDigest,
@@ -1,16 +1,9 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, linkSync, readdirSync, readFileSync, renameSync, unlinkSync } from "node:fs";
3
- import { basename, dirname } from "node:path";
4
1
  import { type Adapter, adapterFromPlatform } from "../adapter.ts";
5
- import {
6
- recordLiveLifecycleChangeV3,
7
- recordLiveTaskChangeV3,
8
- } from "../agents/live-authority-v3.ts";
2
+ import { recordLiveTaskChangeV3 } from "../agents/live-authority-v3.ts";
9
3
  import { endSessionExplicitV3 } from "../agents/session-finalizer-v3.ts";
10
- import { heartbeatPath } from "../agents/state/heartbeat-reader.ts";
11
4
  import {
5
+ clearCoordinationCache,
12
6
  type Heartbeat,
13
- readHeartbeat,
14
7
  setIdentityCache,
15
8
  } from "../agents/state/heartbeat-writer.ts";
16
9
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
@@ -19,17 +12,9 @@ import { assignName } from "../agents/state/names.ts";
19
12
  import { coordFreshnessSeconds } from "../config.ts";
20
13
  import { ensureEventLedgerV3 } from "../events/v3/bootstrap.ts";
21
14
  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,
28
15
  recordLiveHookSignalV3,
29
16
  resolveLiveEventLedgerRouteV3,
30
17
  } from "../events/v3/live-routing.ts";
31
- import { readHookProducerStateV3 } from "../events/v3/producers/recorder.ts";
32
- import { fsyncParentDirectory } from "./durable-record.ts";
33
18
  import { stableScopeId } from "./scope-id.ts";
34
19
 
35
20
  export interface WorkflowChildSessionV3Input {
@@ -92,45 +77,16 @@ export function startWorkflowChildSessionV3(input: WorkflowChildSessionV3Input):
92
77
  }
93
78
  const projected = readLiveCoordinationRow(input.coordRoot, input.instanceId);
94
79
  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
- }
99
80
  return projected;
100
81
  }
101
82
 
102
83
  /** Record the authoritative terminal before removing the disposable coordination cache. */
103
84
  export function endWorkflowChildSessionV3(
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 },
85
+ input: WorkflowChildSessionV3Input & { cleanExit: boolean },
113
86
  ): void {
114
87
  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
- }
132
88
  const row = readLiveCoordinationRow(input.coordRoot, input.instanceId);
133
- if (!row?.v3_instance_id || row.v3_generation_id !== input.expectedGenerationId) {
89
+ if (!row?.v3_instance_id || !row.v3_generation_id) {
134
90
  throw new Error("workflow_child_v3_generation_missing");
135
91
  }
136
92
  const result = endSessionExplicitV3({
@@ -145,104 +101,7 @@ function endExpectedChild(
145
101
  `workflow_child_v3_end_failed:${result.state}${"reason" in result ? `:${result.reason}` : ""}`,
146
102
  );
147
103
  }
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;
104
+ clearCoordinationCache(input.coordRoot, input.instanceId);
246
105
  }
247
106
 
248
107
  function requireV3Route(coordRoot: string) {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { isAbsolute, join } from "node:path";
4
- import { observationsComparable, type RepoSnapshot } from "../context/observation.ts";
3
+ import { join } from "node:path";
4
+ import { type RepoSnapshot, repoComparisonProblems } from "../context/observation.ts";
5
5
  import { DIAGNOSTIC_ADVICE_SCHEMA_VERSION } from "../diagnostics/contract.ts";
6
6
  import type {
7
7
  NormalizedPolicy,
@@ -12,7 +12,6 @@ import type {
12
12
  import { policyDigest } from "../policy/index.ts";
13
13
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
14
14
  import { stableDigest, writeImmutableJson } from "./durable-record.ts";
15
- import { failureEvidenceAllowsUncharged, isSpawnFailureEvidence } from "./failure-evidence.ts";
16
15
  import { readWorkflowRunManifest } from "./run-state.ts";
17
16
  import type {
18
17
  AcceptanceCriterion,
@@ -243,25 +242,17 @@ export function digestResult(value: unknown, kind?: "text" | "json"): ResultDige
243
242
  * attempt was informative about the work — charge it. This also keeps a
244
243
  * resumed run whose earlier segment did real work from being written off by
245
244
  * a later environment failure.
246
- * 3. Every participant must have failed with positive no-effect evidence,
247
- * observed absent output and no refusal. A class alone is insufficient.
248
- * Among those eligible failures, environment is the hard stop.
245
+ * 3. Otherwise, among the failed agents, environment wins over upstream: a
246
+ * missing binary means nothing ran at all, and it is the operator-chosen
247
+ * hard stop.
249
248
  * 4. Anything else is undefined ⇒ a charged work failure, exactly as today.
250
249
  */
251
250
  export function deriveRunFailureClass(
252
251
  status: "succeeded" | "failed",
253
- agents: readonly Pick<WorkflowAgentProof, "status" | "class" | "failure_evidence">[],
252
+ agents: readonly Pick<WorkflowAgentProof, "status" | "class">[],
254
253
  ): SpawnFailureClass | undefined {
255
254
  if (status !== "failed") return undefined;
256
- if (
257
- agents.length === 0 ||
258
- agents.some(
259
- (agent) =>
260
- agent.status !== "failed" ||
261
- (agent.class !== "environment" && agent.class !== "upstream") ||
262
- !failureEvidenceAllowsUncharged(agent.failure_evidence),
263
- )
264
- ) {
255
+ if (agents.some((agent) => agent.status === "succeeded" || agent.status === "cached")) {
265
256
  return undefined;
266
257
  }
267
258
  const failed = agents.filter((agent) => agent.status === "failed");
@@ -394,13 +385,6 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
394
385
  proof.run.class !== "environment" &&
395
386
  proof.run.class !== "upstream" &&
396
387
  proof.run.class !== "decision") ||
397
- !Array.isArray(proof.agents) ||
398
- proof.agents.some(
399
- (agent) =>
400
- !agent ||
401
- typeof agent !== "object" ||
402
- (agent.failure_evidence !== undefined && !isSpawnFailureEvidence(agent.failure_evidence)),
403
- ) ||
404
388
  (proof.run.work_context !== undefined &&
405
389
  (!proof.run.work_item_id ||
406
390
  proof.run.work_context.id !== proof.run.work_item_id ||
@@ -415,15 +399,8 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
415
399
  ) {
416
400
  throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
417
401
  }
418
- if (
419
- repoComparisonProblem(proof.repository.before, proof.repository.after) !== undefined ||
420
- proof.repository.before.dirty_paths_truncated ||
421
- proof.repository.after.dirty_paths_truncated ||
422
- !proof.repository.before.branch?.trim() ||
423
- !proof.repository.after.branch?.trim() ||
424
- !proof.repository.before.head?.trim() ||
425
- !proof.repository.after.head?.trim()
426
- ) {
402
+ // Packets written before observation checks may carry unproven drift.
403
+ if (repoProblems(proof.repository).length > 0) {
427
404
  proof.repository = buildRepoEvidence(proof.repository.before, proof.repository.after);
428
405
  proof.unknowns = proof.unknowns.filter((item) => item.code !== "repository_drift_incomplete");
429
406
  proof.unknowns.push({
@@ -544,29 +521,16 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
544
521
  : "admission: shadow not needed; no real child dispatch",
545
522
  );
546
523
  }
547
- const comparable =
548
- repoComparisonProblem(proof.repository.before, proof.repository.after) === undefined;
549
- const repo =
550
- comparable &&
551
- !proof.repository.before.dirty_paths_truncated &&
552
- !proof.repository.after.dirty_paths_truncated &&
553
- proof.repository.before.branch?.trim() &&
554
- proof.repository.after.branch?.trim() &&
555
- proof.repository.before.head?.trim() &&
556
- proof.repository.after.head?.trim()
557
- ? proof.repository
558
- : buildRepoEvidence(proof.repository.before, proof.repository.after);
559
- const drift = repo.drift;
560
- if (!comparable) {
561
- lines.push(`repository: drift unknown. ${drift.note}`);
524
+ const problems = repoProblems(proof.repository);
525
+ if (problems.length > 0) {
526
+ lines.push(`repository: drift unknown. ${unverifiedDrift(problems)}`);
562
527
  } else {
563
- const paths =
564
- drift.dirty_paths_added === null || drift.dirty_paths_cleared === null
565
- ? "dirty-path drift unknown"
566
- : `${drift.dirty_paths_added.length} dirty added, ${drift.dirty_paths_cleared.length} cleared`;
528
+ const { before, after, drift } = proof.repository;
567
529
  lines.push(
568
- `repository: branch ${repo.before.branch ?? "unknown"} -> ${repo.after.branch ?? "unknown"}; ` +
569
- `HEAD ${short(repo.before.head)} -> ${short(repo.after.head)}; ${paths}`,
530
+ `repository: branch ${before.branch} -> ${after.branch}; ` +
531
+ `HEAD ${short(before.head)} -> ${short(after.head)}; ` +
532
+ `${drift.dirty_paths_added?.length ?? "unknown"} dirty added, ` +
533
+ `${drift.dirty_paths_cleared?.length ?? "unknown"} cleared`,
570
534
  );
571
535
  }
572
536
  if (proof.unknowns.length > 0) {
@@ -580,8 +544,8 @@ export function renderWorkflowProof(proof: WorkflowProof): string {
580
544
  function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): WorkflowRepoEvidence {
581
545
  const before = normalizeRepoSnapshot(beforeRaw);
582
546
  const after = normalizeRepoSnapshot(afterRaw);
583
- const problem = repoComparisonProblem(before, after);
584
- if (problem !== undefined) {
547
+ const problems = repoProblems({ before, after });
548
+ if (problems.length > 0) {
585
549
  return {
586
550
  source: "engine",
587
551
  before,
@@ -593,47 +557,43 @@ function buildRepoEvidence(beforeRaw: RepoSnapshot, afterRaw: RepoSnapshot): Wor
593
557
  dirty_paths_cleared: null,
594
558
  dirty_paths_retained: null,
595
559
  incomplete: true,
596
- note: `Repository drift could not be verified: ${problem}. Re-check before editing.`,
560
+ note: unverifiedDrift(problems),
597
561
  },
598
562
  };
599
563
  }
600
- const pathsComparable = !before.dirty_paths_truncated && !after.dirty_paths_truncated;
601
- const branchKnown = Boolean(before.branch && after.branch);
602
- const headKnown = Boolean(before.head && after.head);
603
564
  const beforeDirty = new Set(before.dirty_paths);
604
565
  const afterDirty = new Set(after.dirty_paths);
605
566
  const retained = after.dirty_paths.filter((path) => beforeDirty.has(path));
606
- const notes = [
607
- ...(!pathsComparable
608
- ? ["Dirty-path drift is unknown because a snapshot list was truncated."]
609
- : []),
610
- ...(!branchKnown ? ["Branch drift is unknown because a snapshot has no branch."] : []),
611
- ...(!headKnown ? ["HEAD drift is unknown because a snapshot has no HEAD."] : []),
612
- ...(pathsComparable && retained.length > 0
613
- ? ["Snapshots cannot prove whether retained dirty paths changed during the run."]
614
- : []),
615
- ];
616
- const incomplete = notes.length > 0;
567
+ const incomplete = retained.length > 0;
617
568
  return {
618
569
  source: "engine",
619
570
  before,
620
571
  after,
621
572
  drift: {
622
- branch_changed: branchKnown ? before.branch !== after.branch : null,
623
- head_changed: headKnown ? before.head !== after.head : null,
624
- dirty_paths_added: pathsComparable
625
- ? after.dirty_paths.filter((path) => !beforeDirty.has(path))
626
- : null,
627
- dirty_paths_cleared: pathsComparable
628
- ? before.dirty_paths.filter((path) => !afterDirty.has(path))
629
- : null,
630
- dirty_paths_retained: pathsComparable ? retained : null,
573
+ branch_changed: before.branch !== after.branch,
574
+ head_changed: before.head !== after.head,
575
+ dirty_paths_added: after.dirty_paths.filter((path) => !beforeDirty.has(path)),
576
+ dirty_paths_cleared: before.dirty_paths.filter((path) => !afterDirty.has(path)),
577
+ dirty_paths_retained: retained,
631
578
  incomplete,
632
- note: incomplete ? notes.join(" ") : undefined,
579
+ note: incomplete
580
+ ? "Snapshots cannot prove whether retained dirty paths changed during the run."
581
+ : undefined,
633
582
  },
634
583
  };
635
584
  }
636
585
 
586
+ function repoProblems(repository: {
587
+ before: WorkflowRepoSnapshot;
588
+ after: WorkflowRepoSnapshot;
589
+ }): string[] {
590
+ return repoComparisonProblems(repository.before, repository.after, ["before", "after"]);
591
+ }
592
+
593
+ function unverifiedDrift(problems: string[]): string {
594
+ return `Repository drift could not be verified: ${problems.join("; ")}. Re-check before editing.`;
595
+ }
596
+
637
597
  function normalizeRepoSnapshot(snapshot: RepoSnapshot): WorkflowRepoSnapshot {
638
598
  return {
639
599
  cwd: snapshot.cwd,
@@ -649,30 +609,6 @@ function normalizeRepoSnapshot(snapshot: RepoSnapshot): WorkflowRepoSnapshot {
649
609
  };
650
610
  }
651
611
 
652
- function snapshotFailure(label: string, snapshot: WorkflowRepoSnapshot): string {
653
- return `${label} snapshot is ${snapshot.observation}${snapshot.observation_detail ? ` (${snapshot.observation_detail})` : ""}`;
654
- }
655
-
656
- function repoComparisonProblem(
657
- before: WorkflowRepoSnapshot,
658
- after: WorkflowRepoSnapshot,
659
- ): string | undefined {
660
- if (!observationsComparable(before, after)) {
661
- return [
662
- ...(before.observation !== "ok" ? [snapshotFailure("before", before)] : []),
663
- ...(after.observation !== "ok" ? [snapshotFailure("after", after)] : []),
664
- ].join("; ");
665
- }
666
- const unknownRoots = [
667
- ...(!before.root || !isAbsolute(before.root) ? ["before"] : []),
668
- ...(!after.root || !isAbsolute(after.root) ? ["after"] : []),
669
- ];
670
- if (unknownRoots.length > 0)
671
- return `${unknownRoots.join(" and ")} snapshot repository root is unknown`;
672
- if (before.root !== after.root) return "snapshots name different repository roots";
673
- return undefined;
674
- }
675
-
676
612
  function buildAdapterCoverage(
677
613
  agents: WorkflowAgentProof[],
678
614
  claims: Readonly<Record<string, AdapterEvidenceCapability | undefined>> | undefined,