@kontourai/flow-agents 3.11.0 → 3.12.1

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 (254) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/src/builder-flow-run-adapter.d.ts +1 -0
  3. package/build/src/builder-flow-run-adapter.js +2 -0
  4. package/build/src/builder-flow-runtime.d.ts +5 -0
  5. package/build/src/builder-flow-runtime.js +90 -68
  6. package/build/src/builder-gate-action-envelope.d.ts +152 -0
  7. package/build/src/builder-gate-action-envelope.js +368 -0
  8. package/build/src/cli/public-contracts.d.ts +139 -0
  9. package/build/src/cli/public-contracts.js +37 -0
  10. package/build/src/cli/workflow-sidecar.js +9 -13
  11. package/build/src/cli/workflow.d.ts +1 -0
  12. package/build/src/cli/workflow.js +49 -12
  13. package/build/src/continuation-driver.d.ts +40 -3
  14. package/build/src/continuation-driver.js +361 -412
  15. package/build/src/continuation-persistence.d.ts +4 -0
  16. package/build/src/continuation-persistence.js +296 -0
  17. package/build/src/continuation-validation.d.ts +9 -0
  18. package/build/src/continuation-validation.js +169 -0
  19. package/build/src/flow-kit/action-metadata.d.ts +28 -0
  20. package/build/src/flow-kit/action-metadata.js +162 -0
  21. package/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  22. package/build/src/flow-kit/action-repository-validation.js +237 -0
  23. package/build/src/flow-kit/validate.d.ts +2 -14
  24. package/build/src/flow-kit/validate.js +18 -345
  25. package/build/src/index.d.ts +4 -2
  26. package/build/src/index.js +2 -1
  27. package/dist/base/build/package.json +1 -1
  28. package/dist/base/build/src/builder-flow-run-adapter.d.ts +1 -0
  29. package/dist/base/build/src/builder-flow-run-adapter.js +2 -0
  30. package/dist/base/build/src/builder-flow-runtime.d.ts +5 -0
  31. package/dist/base/build/src/builder-flow-runtime.js +90 -68
  32. package/dist/base/build/src/builder-gate-action-envelope.d.ts +152 -0
  33. package/dist/base/build/src/builder-gate-action-envelope.js +368 -0
  34. package/dist/base/build/src/cli/public-contracts.d.ts +139 -0
  35. package/dist/base/build/src/cli/public-contracts.js +37 -0
  36. package/dist/base/build/src/cli/workflow-sidecar.js +9 -13
  37. package/dist/base/build/src/cli/workflow.d.ts +1 -0
  38. package/dist/base/build/src/cli/workflow.js +49 -12
  39. package/dist/base/build/src/continuation-driver.d.ts +40 -3
  40. package/dist/base/build/src/continuation-driver.js +361 -412
  41. package/dist/base/build/src/continuation-persistence.d.ts +4 -0
  42. package/dist/base/build/src/continuation-persistence.js +296 -0
  43. package/dist/base/build/src/continuation-validation.d.ts +9 -0
  44. package/dist/base/build/src/continuation-validation.js +169 -0
  45. package/dist/base/build/src/flow-kit/action-metadata.d.ts +28 -0
  46. package/dist/base/build/src/flow-kit/action-metadata.js +162 -0
  47. package/dist/base/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  48. package/dist/base/build/src/flow-kit/action-repository-validation.js +237 -0
  49. package/dist/base/build/src/flow-kit/validate.d.ts +2 -14
  50. package/dist/base/build/src/flow-kit/validate.js +18 -345
  51. package/dist/base/build/src/index.d.ts +4 -2
  52. package/dist/base/build/src/index.js +2 -1
  53. package/dist/base/docs/migrations.md +5 -0
  54. package/dist/base/docs/public-workflow-cli.md +41 -5
  55. package/dist/base/docs/spec/builder-flow-runtime.md +117 -0
  56. package/dist/base/evals/integration/test_builder_step_producers.sh +60 -21
  57. package/dist/base/evals/integration/test_bundle_install.sh +15 -15
  58. package/dist/base/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  59. package/dist/base/install.sh +1 -1
  60. package/dist/base/kits/builder/kit.json +88 -14
  61. package/dist/claude-code/build/package.json +1 -1
  62. package/dist/claude-code/build/src/builder-flow-run-adapter.d.ts +1 -0
  63. package/dist/claude-code/build/src/builder-flow-run-adapter.js +2 -0
  64. package/dist/claude-code/build/src/builder-flow-runtime.d.ts +5 -0
  65. package/dist/claude-code/build/src/builder-flow-runtime.js +90 -68
  66. package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +152 -0
  67. package/dist/claude-code/build/src/builder-gate-action-envelope.js +368 -0
  68. package/dist/claude-code/build/src/cli/public-contracts.d.ts +139 -0
  69. package/dist/claude-code/build/src/cli/public-contracts.js +37 -0
  70. package/dist/claude-code/build/src/cli/workflow-sidecar.js +9 -13
  71. package/dist/claude-code/build/src/cli/workflow.d.ts +1 -0
  72. package/dist/claude-code/build/src/cli/workflow.js +49 -12
  73. package/dist/claude-code/build/src/continuation-driver.d.ts +40 -3
  74. package/dist/claude-code/build/src/continuation-driver.js +361 -412
  75. package/dist/claude-code/build/src/continuation-persistence.d.ts +4 -0
  76. package/dist/claude-code/build/src/continuation-persistence.js +296 -0
  77. package/dist/claude-code/build/src/continuation-validation.d.ts +9 -0
  78. package/dist/claude-code/build/src/continuation-validation.js +169 -0
  79. package/dist/claude-code/build/src/flow-kit/action-metadata.d.ts +28 -0
  80. package/dist/claude-code/build/src/flow-kit/action-metadata.js +162 -0
  81. package/dist/claude-code/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  82. package/dist/claude-code/build/src/flow-kit/action-repository-validation.js +237 -0
  83. package/dist/claude-code/build/src/flow-kit/validate.d.ts +2 -14
  84. package/dist/claude-code/build/src/flow-kit/validate.js +18 -345
  85. package/dist/claude-code/build/src/index.d.ts +4 -2
  86. package/dist/claude-code/build/src/index.js +2 -1
  87. package/dist/claude-code/docs/migrations.md +5 -0
  88. package/dist/claude-code/docs/public-workflow-cli.md +41 -5
  89. package/dist/claude-code/docs/spec/builder-flow-runtime.md +117 -0
  90. package/dist/claude-code/evals/integration/test_builder_step_producers.sh +60 -21
  91. package/dist/claude-code/evals/integration/test_bundle_install.sh +15 -15
  92. package/dist/claude-code/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  93. package/dist/claude-code/install.sh +1 -1
  94. package/dist/claude-code/kits/builder/kit.json +88 -14
  95. package/dist/codex/build/package.json +1 -1
  96. package/dist/codex/build/src/builder-flow-run-adapter.d.ts +1 -0
  97. package/dist/codex/build/src/builder-flow-run-adapter.js +2 -0
  98. package/dist/codex/build/src/builder-flow-runtime.d.ts +5 -0
  99. package/dist/codex/build/src/builder-flow-runtime.js +90 -68
  100. package/dist/codex/build/src/builder-gate-action-envelope.d.ts +152 -0
  101. package/dist/codex/build/src/builder-gate-action-envelope.js +368 -0
  102. package/dist/codex/build/src/cli/public-contracts.d.ts +139 -0
  103. package/dist/codex/build/src/cli/public-contracts.js +37 -0
  104. package/dist/codex/build/src/cli/workflow-sidecar.js +9 -13
  105. package/dist/codex/build/src/cli/workflow.d.ts +1 -0
  106. package/dist/codex/build/src/cli/workflow.js +49 -12
  107. package/dist/codex/build/src/continuation-driver.d.ts +40 -3
  108. package/dist/codex/build/src/continuation-driver.js +361 -412
  109. package/dist/codex/build/src/continuation-persistence.d.ts +4 -0
  110. package/dist/codex/build/src/continuation-persistence.js +296 -0
  111. package/dist/codex/build/src/continuation-validation.d.ts +9 -0
  112. package/dist/codex/build/src/continuation-validation.js +169 -0
  113. package/dist/codex/build/src/flow-kit/action-metadata.d.ts +28 -0
  114. package/dist/codex/build/src/flow-kit/action-metadata.js +162 -0
  115. package/dist/codex/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  116. package/dist/codex/build/src/flow-kit/action-repository-validation.js +237 -0
  117. package/dist/codex/build/src/flow-kit/validate.d.ts +2 -14
  118. package/dist/codex/build/src/flow-kit/validate.js +18 -345
  119. package/dist/codex/build/src/index.d.ts +4 -2
  120. package/dist/codex/build/src/index.js +2 -1
  121. package/dist/codex/docs/migrations.md +5 -0
  122. package/dist/codex/docs/public-workflow-cli.md +41 -5
  123. package/dist/codex/docs/spec/builder-flow-runtime.md +117 -0
  124. package/dist/codex/evals/integration/test_builder_step_producers.sh +60 -21
  125. package/dist/codex/evals/integration/test_bundle_install.sh +15 -15
  126. package/dist/codex/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  127. package/dist/codex/install.sh +1 -1
  128. package/dist/codex/kits/builder/kit.json +88 -14
  129. package/dist/kiro/build/package.json +1 -1
  130. package/dist/kiro/build/src/builder-flow-run-adapter.d.ts +1 -0
  131. package/dist/kiro/build/src/builder-flow-run-adapter.js +2 -0
  132. package/dist/kiro/build/src/builder-flow-runtime.d.ts +5 -0
  133. package/dist/kiro/build/src/builder-flow-runtime.js +90 -68
  134. package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +152 -0
  135. package/dist/kiro/build/src/builder-gate-action-envelope.js +368 -0
  136. package/dist/kiro/build/src/cli/public-contracts.d.ts +139 -0
  137. package/dist/kiro/build/src/cli/public-contracts.js +37 -0
  138. package/dist/kiro/build/src/cli/workflow-sidecar.js +9 -13
  139. package/dist/kiro/build/src/cli/workflow.d.ts +1 -0
  140. package/dist/kiro/build/src/cli/workflow.js +49 -12
  141. package/dist/kiro/build/src/continuation-driver.d.ts +40 -3
  142. package/dist/kiro/build/src/continuation-driver.js +361 -412
  143. package/dist/kiro/build/src/continuation-persistence.d.ts +4 -0
  144. package/dist/kiro/build/src/continuation-persistence.js +296 -0
  145. package/dist/kiro/build/src/continuation-validation.d.ts +9 -0
  146. package/dist/kiro/build/src/continuation-validation.js +169 -0
  147. package/dist/kiro/build/src/flow-kit/action-metadata.d.ts +28 -0
  148. package/dist/kiro/build/src/flow-kit/action-metadata.js +162 -0
  149. package/dist/kiro/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  150. package/dist/kiro/build/src/flow-kit/action-repository-validation.js +237 -0
  151. package/dist/kiro/build/src/flow-kit/validate.d.ts +2 -14
  152. package/dist/kiro/build/src/flow-kit/validate.js +18 -345
  153. package/dist/kiro/build/src/index.d.ts +4 -2
  154. package/dist/kiro/build/src/index.js +2 -1
  155. package/dist/kiro/docs/migrations.md +5 -0
  156. package/dist/kiro/docs/public-workflow-cli.md +41 -5
  157. package/dist/kiro/docs/spec/builder-flow-runtime.md +117 -0
  158. package/dist/kiro/evals/integration/test_builder_step_producers.sh +60 -21
  159. package/dist/kiro/evals/integration/test_bundle_install.sh +15 -15
  160. package/dist/kiro/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  161. package/dist/kiro/install.sh +1 -1
  162. package/dist/kiro/kits/builder/kit.json +88 -14
  163. package/dist/opencode/build/package.json +1 -1
  164. package/dist/opencode/build/src/builder-flow-run-adapter.d.ts +1 -0
  165. package/dist/opencode/build/src/builder-flow-run-adapter.js +2 -0
  166. package/dist/opencode/build/src/builder-flow-runtime.d.ts +5 -0
  167. package/dist/opencode/build/src/builder-flow-runtime.js +90 -68
  168. package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +152 -0
  169. package/dist/opencode/build/src/builder-gate-action-envelope.js +368 -0
  170. package/dist/opencode/build/src/cli/public-contracts.d.ts +139 -0
  171. package/dist/opencode/build/src/cli/public-contracts.js +37 -0
  172. package/dist/opencode/build/src/cli/workflow-sidecar.js +9 -13
  173. package/dist/opencode/build/src/cli/workflow.d.ts +1 -0
  174. package/dist/opencode/build/src/cli/workflow.js +49 -12
  175. package/dist/opencode/build/src/continuation-driver.d.ts +40 -3
  176. package/dist/opencode/build/src/continuation-driver.js +361 -412
  177. package/dist/opencode/build/src/continuation-persistence.d.ts +4 -0
  178. package/dist/opencode/build/src/continuation-persistence.js +296 -0
  179. package/dist/opencode/build/src/continuation-validation.d.ts +9 -0
  180. package/dist/opencode/build/src/continuation-validation.js +169 -0
  181. package/dist/opencode/build/src/flow-kit/action-metadata.d.ts +28 -0
  182. package/dist/opencode/build/src/flow-kit/action-metadata.js +162 -0
  183. package/dist/opencode/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  184. package/dist/opencode/build/src/flow-kit/action-repository-validation.js +237 -0
  185. package/dist/opencode/build/src/flow-kit/validate.d.ts +2 -14
  186. package/dist/opencode/build/src/flow-kit/validate.js +18 -345
  187. package/dist/opencode/build/src/index.d.ts +4 -2
  188. package/dist/opencode/build/src/index.js +2 -1
  189. package/dist/opencode/docs/migrations.md +5 -0
  190. package/dist/opencode/docs/public-workflow-cli.md +41 -5
  191. package/dist/opencode/docs/spec/builder-flow-runtime.md +117 -0
  192. package/dist/opencode/evals/integration/test_builder_step_producers.sh +60 -21
  193. package/dist/opencode/evals/integration/test_bundle_install.sh +15 -15
  194. package/dist/opencode/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  195. package/dist/opencode/install.sh +1 -1
  196. package/dist/opencode/kits/builder/kit.json +88 -14
  197. package/dist/pi/build/package.json +1 -1
  198. package/dist/pi/build/src/builder-flow-run-adapter.d.ts +1 -0
  199. package/dist/pi/build/src/builder-flow-run-adapter.js +2 -0
  200. package/dist/pi/build/src/builder-flow-runtime.d.ts +5 -0
  201. package/dist/pi/build/src/builder-flow-runtime.js +90 -68
  202. package/dist/pi/build/src/builder-gate-action-envelope.d.ts +152 -0
  203. package/dist/pi/build/src/builder-gate-action-envelope.js +368 -0
  204. package/dist/pi/build/src/cli/public-contracts.d.ts +139 -0
  205. package/dist/pi/build/src/cli/public-contracts.js +37 -0
  206. package/dist/pi/build/src/cli/workflow-sidecar.js +9 -13
  207. package/dist/pi/build/src/cli/workflow.d.ts +1 -0
  208. package/dist/pi/build/src/cli/workflow.js +49 -12
  209. package/dist/pi/build/src/continuation-driver.d.ts +40 -3
  210. package/dist/pi/build/src/continuation-driver.js +361 -412
  211. package/dist/pi/build/src/continuation-persistence.d.ts +4 -0
  212. package/dist/pi/build/src/continuation-persistence.js +296 -0
  213. package/dist/pi/build/src/continuation-validation.d.ts +9 -0
  214. package/dist/pi/build/src/continuation-validation.js +169 -0
  215. package/dist/pi/build/src/flow-kit/action-metadata.d.ts +28 -0
  216. package/dist/pi/build/src/flow-kit/action-metadata.js +162 -0
  217. package/dist/pi/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  218. package/dist/pi/build/src/flow-kit/action-repository-validation.js +237 -0
  219. package/dist/pi/build/src/flow-kit/validate.d.ts +2 -14
  220. package/dist/pi/build/src/flow-kit/validate.js +18 -345
  221. package/dist/pi/build/src/index.d.ts +4 -2
  222. package/dist/pi/build/src/index.js +2 -1
  223. package/dist/pi/docs/migrations.md +5 -0
  224. package/dist/pi/docs/public-workflow-cli.md +41 -5
  225. package/dist/pi/docs/spec/builder-flow-runtime.md +117 -0
  226. package/dist/pi/evals/integration/test_builder_step_producers.sh +60 -21
  227. package/dist/pi/evals/integration/test_bundle_install.sh +15 -15
  228. package/dist/pi/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  229. package/dist/pi/install.sh +1 -1
  230. package/dist/pi/kits/builder/kit.json +88 -14
  231. package/docs/migrations.md +5 -0
  232. package/docs/public-workflow-cli.md +41 -5
  233. package/docs/spec/builder-flow-runtime.md +117 -0
  234. package/evals/integration/test_builder_step_producers.sh +60 -21
  235. package/evals/integration/test_bundle_install.sh +15 -15
  236. package/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  237. package/kits/builder/kit.json +88 -14
  238. package/package.json +1 -1
  239. package/src/builder-flow-run-adapter.ts +3 -0
  240. package/src/builder-flow-runtime.ts +73 -46
  241. package/src/builder-gate-action-envelope.ts +531 -0
  242. package/src/cli/builder-flow-runtime.test.mjs +289 -58
  243. package/src/cli/continuation-driver.test.mjs +542 -24
  244. package/src/cli/kit-metadata-security.test.mjs +161 -8
  245. package/src/cli/public-contracts.ts +41 -0
  246. package/src/cli/workflow-sidecar.ts +9 -13
  247. package/src/cli/workflow.ts +48 -12
  248. package/src/continuation-driver.ts +466 -378
  249. package/src/continuation-persistence.ts +228 -0
  250. package/src/continuation-validation.ts +137 -0
  251. package/src/flow-kit/action-metadata.ts +155 -0
  252. package/src/flow-kit/action-repository-validation.ts +192 -0
  253. package/src/flow-kit/validate.ts +19 -326
  254. package/src/index.ts +17 -0
@@ -1,9 +1,17 @@
1
- import * as fs from "node:fs";
2
- import { constants as fsConstants } from "node:fs";
3
- import { randomUUID } from "node:crypto";
4
1
  import * as path from "node:path";
5
2
  import { inspectBuilderFlowSession, syncBuilderFlowSession } from "./builder-flow-runtime.js";
6
- import { atomicWriteJson, ensureSafeDirectory } from "./lib/fs.js";
3
+ import {
4
+ gateActionProgressSnapshot,
5
+ withGateActionPriorProgress,
6
+ type GateActionEnvelope,
7
+ type GateActionPriorProgress,
8
+ type GateActionProgressSnapshot,
9
+ } from "./builder-gate-action-envelope.js";
10
+ import { createFileContinuationStore } from "./continuation-persistence.js";
11
+ import { assertMaxTurns, assertMissionIdentity, validateSnapshot, validateState, validateTurnResult } from "./continuation-validation.js";
12
+
13
+ export { MAX_CONTINUATION_TURNS, createFileContinuationStore, withContinuationDriverLock } from "./continuation-persistence.js";
14
+ export { MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES, MAX_CONTINUATION_TURN_RESULT_BYTES } from "./continuation-validation.js";
7
15
 
8
16
  export type ContinuationBarrier =
9
17
  | { kind: "pid"; pid: number }
@@ -16,6 +24,10 @@ export type ContinuationSnapshot = {
16
24
  disposition: "continue" | "waiting" | "done" | "failed";
17
25
  current_step: string;
18
26
  next_action: Record<string, unknown> | null;
27
+ /** Additive Builder-specific execution context. Generic adapters may ignore it. */
28
+ gate_action_envelope?: GateActionEnvelope;
29
+ /** Canonical progress observation, available even when no action envelope is emitted. */
30
+ progress_snapshot?: GateActionProgressSnapshot;
19
31
  };
20
32
 
21
33
  export type ContinuationTurnRequest = {
@@ -26,12 +38,15 @@ export type ContinuationTurnRequest = {
26
38
  iteration: number;
27
39
  max_turns: number;
28
40
  next_action: Record<string, unknown> | null;
41
+ /** Additive schema-1.0 field; older external adapters remain valid. */
42
+ gate_action_envelope?: GateActionEnvelope;
29
43
  };
30
44
 
31
45
  export type ContinuationTurnResult =
32
46
  | { status: "completed"; summary?: string; evidence?: Record<string, unknown> }
33
47
  | { status: "wait"; barrier: ContinuationBarrier; summary?: string };
34
48
 
49
+
35
50
  export class ContinuationAdapterTimeoutError extends Error {
36
51
  readonly code = "CONTINUATION_ADAPTER_TIMEOUT";
37
52
 
@@ -53,13 +68,21 @@ export type ContinuationDriverState = {
53
68
  active_turn_step?: string | null;
54
69
  // Anchors the ephemeral signer outside active-turn.json. Legacy state files may omit it.
55
70
  active_turn_public_key_digest?: string | null;
71
+ /** Durable recovery marker for an adapter turn without a post-turn measurement. */
72
+ active_turn_phase?: "prepared" | "started" | "measured" | null;
73
+ active_turn_progress?: GateActionProgressSnapshot | null;
74
+ /** Crash-recovery journal entry retained until capture and audit are durable. */
75
+ active_turn_capture?: ContinuationAcceptedTurn | null;
76
+ /** Last canonical progress observed after an adapter turn. Additive to state schema 1.0. */
77
+ last_progress?: GateActionProgressSnapshot | null;
78
+ prior_progress?: GateActionPriorProgress | null;
56
79
  pending_barrier: ContinuationBarrier | null;
57
80
  updated_at: string;
58
81
  };
59
82
 
60
83
  export type ContinuationDriverEvent = {
61
84
  schema_version: "1.0";
62
- type: "started" | "turn_started" | "turn_completed" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
85
+ type: "started" | "turn_started" | "turn_completed" | "turn_recovered" | "gate_not_advanced" | "turn_failed" | "authority_cleanup_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
63
86
  run_id: string;
64
87
  definition_id: string;
65
88
  current_step: string;
@@ -68,6 +91,18 @@ export type ContinuationDriverEvent = {
68
91
  barrier?: ContinuationBarrier;
69
92
  summary?: string;
70
93
  failure_kind?: "adapter_error" | "timeout";
94
+ progress?: GateActionPriorProgress;
95
+ turn_id?: string;
96
+ };
97
+
98
+ export type ContinuationAcceptedTurn = {
99
+ schema_version: "1.0";
100
+ turn_id: string;
101
+ iteration: number;
102
+ request: ContinuationTurnRequest;
103
+ result: ContinuationTurnResult;
104
+ progress: GateActionPriorProgress | null;
105
+ captured_at: string;
71
106
  };
72
107
 
73
108
  export interface ContinuationStateStore {
@@ -76,6 +111,15 @@ export interface ContinuationStateStore {
76
111
  append(event: ContinuationDriverEvent): void;
77
112
  }
78
113
 
114
+ /**
115
+ * Optional durable accepted-turn journal. Kept separate so existing stores that
116
+ * only implement load/save/append remain valid continuation drivers.
117
+ */
118
+ export interface ContinuationAcceptedTurnJournal {
119
+ captureAcceptedTurn(turn: ContinuationAcceptedTurn): void;
120
+ acceptedTurns(): ContinuationAcceptedTurn[];
121
+ }
122
+
79
123
  export interface ContinuationRuntimePort {
80
124
  inspect(): Promise<ContinuationSnapshot>;
81
125
  synchronize(): Promise<ContinuationSnapshot>;
@@ -115,6 +159,7 @@ export interface RunContinuationDriverInput {
115
159
  waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
116
160
  authorizeTurn?: () => Promise<void>;
117
161
  issueTurnAuthority?: (request: ContinuationTurnRequest) => Promise<ContinuationTurnAuthority>;
162
+ preflightTurn?: (request: ContinuationTurnRequest) => void | Promise<void>;
118
163
  onTurnAccepted?: (request: ContinuationTurnRequest, result: ContinuationTurnResult) => void | Promise<void>;
119
164
  now?: () => Date;
120
165
  }
@@ -127,153 +172,298 @@ export interface DriveBuilderFlowSessionInput {
127
172
  waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
128
173
  authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
129
174
  issueTurnAuthority?: RunContinuationDriverInput["issueTurnAuthority"];
175
+ preflightTurn?: RunContinuationDriverInput["preflightTurn"];
130
176
  onTurnAccepted?: RunContinuationDriverInput["onTurnAccepted"];
131
177
  now?: () => Date;
178
+ store?: ContinuationStateStore;
132
179
  }
133
180
 
134
181
  export async function runContinuationDriver(input: RunContinuationDriverInput): Promise<ContinuationDriverOutcome> {
135
182
  assertMaxTurns(input.maxTurns);
136
183
  const now = input.now ?? (() => new Date());
137
- const waitForBarrier = input.waitForBarrier ?? (async () => "pending" as const);
138
- const authorizeTurn = input.authorizeTurn ?? (async () => {});
139
- const issueTurnAuthority = input.issueTurnAuthority;
140
184
  const adapterCommandIdentity = input.adapterCommandIdentity ?? null;
141
- let snapshot = validateSnapshot(await input.runtime.inspect());
142
- let state = loadOrCreateState(input.store, snapshot, input.maxTurns, adapterCommandIdentity, now);
185
+ const inspected = validateSnapshot(await input.runtime.inspect());
186
+ let state = loadOrCreateState(input.store, inspected, input.maxTurns, adapterCommandIdentity, now);
143
187
  if (state.max_turns !== input.maxTurns) throw new Error(`continuation maxTurns ${input.maxTurns} does not match the persisted mission budget ${state.max_turns}`);
144
188
  if (state.adapter_command_identity !== adapterCommandIdentity) throw new Error("continuation adapter command identity does not match the persisted mission adapter");
189
+ let settled = await settleMissionStart(input, state, now);
190
+ if (settled.outcome) return settled.outcome;
191
+ ({ state } = settled);
192
+ let snapshot = settled.snapshot;
193
+ while (state.turns_started < input.maxTurns) {
194
+ const turn = await executeContinuationTurn(input, state, snapshot, now);
195
+ if (turn.outcome) return turn.outcome;
196
+ state = turn.state;
197
+ snapshot = turn.snapshot;
198
+ }
199
+ return finishBudgetExhausted(input.store, state, snapshot, now);
200
+ }
145
201
 
146
- const inspectedOutcome = canonicalOutcome(snapshot);
147
- if (inspectedOutcome === "done") return finishDone(input.store, state, snapshot, now);
148
- if (inspectedOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
202
+ type MissionPosition = { state: ContinuationDriverState; snapshot: ContinuationSnapshot; outcome?: ContinuationDriverOutcome };
203
+ type TurnCaptureFailure = Error & { cause: unknown; capture_failure: true };
149
204
 
150
- if (state.pending_barrier) {
151
- const barrier = state.pending_barrier;
152
- const readiness = await waitForBarrier(barrier);
205
+ async function settleMissionStart(input: RunContinuationDriverInput, state: ContinuationDriverState, now: () => Date): Promise<MissionPosition> {
206
+ let position = await synchronizeAndRecover(input, state, now);
207
+ if (position.state.pending_barrier && new Set(["done", "failed"]).has(canonicalOutcome(position.snapshot))) {
208
+ const barrier = position.state.pending_barrier;
209
+ position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
210
+ appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier, summary: "canonical Flow resolved the pending barrier" });
211
+ }
212
+ const terminal = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, false);
213
+ if (terminal) return { ...position, outcome: terminal };
214
+ if (position.state.pending_barrier) {
215
+ const barrier = position.state.pending_barrier;
216
+ const readiness = await (input.waitForBarrier ?? (async () => "pending" as const))(barrier);
153
217
  if (readiness === "pending") {
154
- state = saveState(input.store, state, { status: "waiting", active_turn_step: null, active_turn_public_key_digest: null }, now);
155
- return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier };
218
+ const waiting = saveState(input.store, position.state, { status: "waiting" }, now);
219
+ return { state: waiting, snapshot: position.snapshot, outcome: { outcome: "waiting", turns_started: waiting.turns_started, snapshot: position.snapshot, barrier } };
156
220
  }
157
- state = saveState(input.store, state, { status: "active", active_turn_step: null, active_turn_public_key_digest: null, pending_barrier: null }, now);
158
- appendEvent(input.store, state, snapshot, "resumed", now, { barrier });
221
+ position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
222
+ appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier });
223
+ position = await synchronizeAndRecover(input, position.state, now);
159
224
  }
225
+ const disposition = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, true);
226
+ return disposition ? { ...position, outcome: disposition } : position;
227
+ }
160
228
 
161
- snapshot = validateSnapshot(await input.runtime.synchronize());
229
+ async function synchronizeAndRecover(input: RunContinuationDriverInput, state: ContinuationDriverState, now: () => Date): Promise<MissionPosition> {
230
+ const snapshot = validateSnapshot(await input.runtime.synchronize());
162
231
  assertMissionIdentity(state, snapshot);
163
- const initialOutcome = canonicalOutcome(snapshot);
164
- if (initialOutcome === "done") return finishDone(input.store, state, snapshot, now);
165
- if (initialOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
166
- if (initialOutcome === "waiting") {
167
- state = saveState(input.store, state, { status: "waiting", active_turn_step: null, active_turn_public_key_digest: null }, now);
168
- return { outcome: "waiting", turns_started: state.turns_started, snapshot };
232
+ const recovered = reconcileInterruptedTurn(input.store, state, snapshot, now);
233
+ return { state: reconcileProgress(input.store, recovered, snapshot, now), snapshot };
234
+ }
235
+
236
+ function resolveCanonicalDisposition(
237
+ store: ContinuationStateStore,
238
+ state: ContinuationDriverState,
239
+ snapshot: ContinuationSnapshot,
240
+ now: () => Date,
241
+ includeWaiting: boolean,
242
+ ): ContinuationDriverOutcome | null {
243
+ const disposition = canonicalOutcome(snapshot);
244
+ if (disposition === "done") return finishDone(store, state, snapshot, now);
245
+ if (disposition === "failed") return finishFailed(store, state, snapshot, now);
246
+ if (includeWaiting && disposition === "waiting") {
247
+ const waiting = saveState(store, state, { status: "waiting" }, now);
248
+ return { outcome: "waiting", turns_started: waiting.turns_started, snapshot };
169
249
  }
250
+ return null;
251
+ }
170
252
 
171
- while (state.turns_started < input.maxTurns) {
172
- await authorizeTurn();
173
- const iteration = state.turns_started + 1;
174
- state = saveState(input.store, state, {
175
- status: "active",
176
- turns_started: iteration,
177
- active_turn_step: snapshot.current_step,
178
- active_turn_public_key_digest: null,
179
- }, now);
180
- appendEvent(input.store, state, snapshot, "turn_started", now);
181
- let result: ContinuationTurnResult;
182
- let request: ContinuationTurnRequest;
183
- let authority: ContinuationTurnAuthority | undefined;
184
- let terminalized = false;
185
- try {
186
- request = Object.freeze({
187
- schema_version: "1.0",
188
- run_id: snapshot.run_id,
189
- definition_id: snapshot.definition_id,
190
- current_step: snapshot.current_step,
191
- iteration,
192
- max_turns: input.maxTurns,
193
- next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
194
- });
195
- authority = issueTurnAuthority ? await issueTurnAuthority(request) : undefined;
196
- if (authority) {
197
- state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
198
- }
199
- result = validateTurnResult(await input.runtime.execute(request, authority ? {
200
- continuationTurnSecret: authority.turnSecret,
201
- continuationRunId: authority.runId,
202
- } : undefined));
203
- } catch (error) {
204
- const summary = boundedErrorMessage(error);
205
- appendEvent(input.store, state, snapshot, "turn_failed", now, { summary, failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error" });
206
- snapshot = validateSnapshot(await input.runtime.synchronize());
207
- assertMissionIdentity(state, snapshot);
208
- const outcome = canonicalOutcome(snapshot);
209
- if (outcome === "done") {
210
- terminalized = true;
211
- return finishDone(input.store, state, snapshot, now);
212
- }
213
- if (outcome === "failed") {
214
- terminalized = true;
215
- return finishFailed(input.store, state, snapshot, now);
216
- }
217
- if (outcome === "waiting") {
218
- state = saveState(input.store, state, { status: "waiting" }, now);
219
- return { outcome: "waiting", turns_started: state.turns_started, snapshot };
220
- }
221
- continue;
222
- } finally {
223
- let cleanupFailure: string | undefined;
224
- try {
225
- if (authority && !authority.cleanup()) cleanupFailure = "continuation turn authority cleanup returned false";
226
- } catch (error) {
227
- cleanupFailure = boundedErrorMessage(error);
228
- } finally {
229
- if (!terminalized) state = saveState(input.store, state, {
230
- active_turn_step: null,
231
- active_turn_public_key_digest: null,
232
- }, now);
233
- if (cleanupFailure) {
234
- try {
235
- appendEvent(input.store, state, snapshot, "authority_cleanup_failed", now, { summary: cleanupFailure });
236
- } catch {
237
- // Audit is best effort. A cleanup extension must not replace the adapter outcome.
238
- }
239
- }
240
- }
241
- }
242
- if (input.onTurnAccepted) await input.onTurnAccepted(request, result);
243
- appendEvent(input.store, state, snapshot, "turn_completed", now, { summary: result.summary });
244
-
245
- if (result.status === "wait") {
246
- state = saveState(input.store, state, { status: "waiting", pending_barrier: result.barrier }, now);
247
- appendEvent(input.store, state, snapshot, "parked", now, { barrier: result.barrier, summary: result.summary });
248
- const readiness = await waitForBarrier(result.barrier);
249
- if (readiness === "pending") {
250
- return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier: result.barrier };
251
- }
252
- state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
253
- appendEvent(input.store, state, snapshot, "resumed", now, { barrier: result.barrier });
254
- }
253
+ async function executeContinuationTurn(
254
+ input: RunContinuationDriverInput,
255
+ current: ContinuationDriverState,
256
+ snapshot: ContinuationSnapshot,
257
+ now: () => Date,
258
+ ): Promise<MissionPosition> {
259
+ await (input.authorizeTurn ?? (async () => {}))();
260
+ const request = continuationTurnRequest(input, current, snapshot);
261
+ if (input.preflightTurn) await input.preflightTurn(request);
262
+ let state = beginContinuationTurn(input.store, current, snapshot, request.iteration, now);
263
+ let authority: ContinuationTurnAuthority | undefined;
264
+ try {
265
+ authority = input.issueTurnAuthority ? await input.issueTurnAuthority(request) : undefined;
266
+ if (authority) state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
267
+ const result = validateTurnResult(await input.runtime.execute(request, authorityContext(authority)));
268
+ return await recordAcceptedTurn(input, state, snapshot, request, result, now);
269
+ } catch (error) {
270
+ const failed = await recordFailedTurn(input, input.store.load() ?? state, snapshot, error, now);
271
+ if (isTurnCaptureFailure(error)) throw error.cause;
272
+ return failed;
273
+ } finally {
274
+ auditAuthorityCleanup(input.store, state, snapshot, authority, now);
275
+ }
276
+ }
255
277
 
256
- snapshot = validateSnapshot(await input.runtime.synchronize());
257
- assertMissionIdentity(state, snapshot);
258
- if (snapshot.status === "active" && snapshot.current_step === request.current_step) {
259
- appendEvent(input.store, state, snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
260
- }
261
- const outcome = canonicalOutcome(snapshot);
262
- if (outcome === "done") return finishDone(input.store, state, snapshot, now);
263
- if (outcome === "failed") return finishFailed(input.store, state, snapshot, now);
264
- if (outcome === "waiting") {
265
- state = saveState(input.store, state, { status: "waiting" }, now);
266
- return { outcome: "waiting", turns_started: state.turns_started, snapshot };
267
- }
278
+ function continuationTurnRequest(input: RunContinuationDriverInput, state: ContinuationDriverState, snapshot: ContinuationSnapshot): ContinuationTurnRequest {
279
+ const envelope = snapshot.gate_action_envelope
280
+ ? withGateActionPriorProgress(snapshot.gate_action_envelope, state.prior_progress ?? initialProgress())
281
+ : undefined;
282
+ return Object.freeze({
283
+ schema_version: "1.0",
284
+ run_id: snapshot.run_id,
285
+ definition_id: snapshot.definition_id,
286
+ current_step: snapshot.current_step,
287
+ iteration: state.turns_started + 1,
288
+ max_turns: input.maxTurns,
289
+ next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
290
+ ...(envelope ? { gate_action_envelope: envelope } : {}),
291
+ });
292
+ }
293
+
294
+ function beginContinuationTurn(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, iteration: number, now: () => Date): ContinuationDriverState {
295
+ let started = saveState(store, state, {
296
+ status: "active", turns_started: iteration, active_turn_step: snapshot.current_step,
297
+ active_turn_public_key_digest: null, active_turn_phase: "prepared", active_turn_progress: progressSnapshot(snapshot), active_turn_capture: null,
298
+ }, now);
299
+ appendEvent(store, started, snapshot, "turn_started", now);
300
+ started = saveState(store, started, { active_turn_phase: "started" }, now);
301
+ return started;
302
+ }
303
+
304
+ function authorityContext(authority: ContinuationTurnAuthority | undefined): ContinuationTurnContext | undefined {
305
+ return authority ? { continuationTurnSecret: authority.turnSecret, continuationRunId: authority.runId } : undefined;
306
+ }
307
+
308
+ async function captureAcceptedTurn(
309
+ callback: NonNullable<RunContinuationDriverInput["onTurnAccepted"]>,
310
+ request: ContinuationTurnRequest,
311
+ result: ContinuationTurnResult,
312
+ ): Promise<void> {
313
+ try {
314
+ await callback(request, result);
315
+ } catch (cause) {
316
+ const error = new Error(boundedErrorMessage(cause)) as TurnCaptureFailure;
317
+ error.cause = cause;
318
+ error.capture_failure = true;
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ function isTurnCaptureFailure(error: unknown): error is TurnCaptureFailure {
324
+ return error instanceof Error && (error as Partial<TurnCaptureFailure>).capture_failure === true;
325
+ }
326
+
327
+ async function recordFailedTurn(
328
+ input: RunContinuationDriverInput,
329
+ state: ContinuationDriverState,
330
+ previous: ContinuationSnapshot,
331
+ error: unknown,
332
+ now: () => Date,
333
+ ): Promise<MissionPosition> {
334
+ const measured = state.active_turn_phase === "measured" && state.active_turn_capture
335
+ ? { state, snapshot: validateSnapshot(await input.runtime.synchronize()), progress: state.active_turn_capture.progress ?? undefined }
336
+ : await synchronizeTurnMeasurement(input, state, previous, null, null, now);
337
+ appendEvent(input.store, measured.state, measured.snapshot, "turn_failed", now, {
338
+ summary: boundedErrorMessage(error),
339
+ failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error",
340
+ ...(measured.progress ? { progress: measured.progress } : {}),
341
+ ...(measured.state.active_turn_capture ? { turn_id: measured.state.active_turn_capture.turn_id } : {}),
342
+ });
343
+ const cleared = clearActiveTurn(input.store, measured.state, now);
344
+ const outcome = resolveCanonicalDisposition(input.store, cleared, measured.snapshot, now, true);
345
+ return outcome ? { state: cleared, snapshot: measured.snapshot, outcome } : { ...measured, state: cleared };
346
+ }
347
+
348
+ async function recordAcceptedTurn(
349
+ input: RunContinuationDriverInput,
350
+ state: ContinuationDriverState,
351
+ previous: ContinuationSnapshot,
352
+ request: ContinuationTurnRequest,
353
+ result: ContinuationTurnResult,
354
+ now: () => Date,
355
+ ): Promise<MissionPosition> {
356
+ let measured = await synchronizeTurnMeasurement(input, state, previous, request, result, now);
357
+ const capture = measured.state.active_turn_capture!;
358
+ acceptedTurnJournal(input.store)?.captureAcceptedTurn(capture);
359
+ if (input.onTurnAccepted) await captureAcceptedTurn(input.onTurnAccepted, request, result);
360
+ if (result.status === "wait") return parkAcceptedTurn(input, measured, result, now);
361
+ appendEvent(input.store, measured.state, measured.snapshot, "turn_completed", now, {
362
+ summary: result.summary,
363
+ ...(measured.progress ? { progress: measured.progress } : {}),
364
+ turn_id: capture.turn_id,
365
+ });
366
+ measured = { ...measured, state: clearActiveTurn(input.store, measured.state, now) };
367
+ if (measured.snapshot.status === "active" && measured.snapshot.current_step === request.current_step) {
368
+ appendEvent(input.store, measured.state, measured.snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
369
+ }
370
+ const outcome = resolveCanonicalDisposition(input.store, measured.state, measured.snapshot, now, true);
371
+ return outcome ? { state: measured.state, snapshot: measured.snapshot, outcome } : measured;
372
+ }
373
+
374
+ async function parkAcceptedTurn(
375
+ input: RunContinuationDriverInput,
376
+ measured: MissionPosition & { progress?: GateActionPriorProgress },
377
+ result: Extract<ContinuationTurnResult, { status: "wait" }>,
378
+ now: () => Date,
379
+ ): Promise<MissionPosition> {
380
+ let state = saveState(input.store, measured.state, { status: "waiting", pending_barrier: result.barrier }, now);
381
+ const turnId = state.active_turn_capture!.turn_id;
382
+ appendEvent(input.store, state, measured.snapshot, "parked", now, {
383
+ barrier: result.barrier, summary: result.summary, ...(measured.progress ? { progress: measured.progress } : {}), turn_id: turnId,
384
+ });
385
+ state = clearActiveTurn(input.store, state, now);
386
+ const readiness = await (input.waitForBarrier ?? (async () => "pending" as const))(result.barrier);
387
+ if (readiness === "pending") {
388
+ return { state, snapshot: measured.snapshot, outcome: { outcome: "waiting", turns_started: state.turns_started, snapshot: measured.snapshot, barrier: result.barrier } };
268
389
  }
390
+ state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
391
+ appendEvent(input.store, state, measured.snapshot, "resumed", now, { barrier: result.barrier });
392
+ const resumed = await synchronizeAndRecover(input, state, now);
393
+ const outcome = resolveCanonicalDisposition(input.store, resumed.state, resumed.snapshot, now, true);
394
+ return outcome ? { ...resumed, outcome } : resumed;
395
+ }
269
396
 
270
- state = saveState(input.store, state, {
271
- status: "budget_exhausted",
397
+ async function synchronizeTurnMeasurement(
398
+ input: RunContinuationDriverInput,
399
+ state: ContinuationDriverState,
400
+ previous: ContinuationSnapshot,
401
+ request: ContinuationTurnRequest | null,
402
+ result: ContinuationTurnResult | null,
403
+ now: () => Date,
404
+ ): Promise<MissionPosition & { progress?: GateActionPriorProgress }> {
405
+ const snapshot = validateSnapshot(await input.runtime.synchronize());
406
+ assertMissionIdentity(state, snapshot);
407
+ const before = state.active_turn_progress ?? state.last_progress ?? progressSnapshot(previous);
408
+ const progress = measureProgress(state, before, progressSnapshot(snapshot));
409
+ const capture = request && result ? acceptedTurnCapture(request, result, progress?.delta ?? null, now) : null;
410
+ const measured = saveState(input.store, state, {
411
+ ...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
412
+ ...(capture ? { active_turn_phase: "measured", active_turn_capture: capture } : {}),
413
+ }, now);
414
+ return { state: measured, snapshot, ...(progress ? { progress: progress.delta } : {}) };
415
+ }
416
+
417
+ function acceptedTurnCapture(request: ContinuationTurnRequest, result: ContinuationTurnResult, progress: GateActionPriorProgress | null, now: () => Date): ContinuationAcceptedTurn {
418
+ return {
419
+ schema_version: "1.0",
420
+ turn_id: `${request.run_id}:${request.iteration}`,
421
+ iteration: request.iteration,
422
+ request: structuredClone(request),
423
+ result: structuredClone(result),
424
+ progress: progress ? structuredClone(progress) : null,
425
+ captured_at: now().toISOString(),
426
+ };
427
+ }
428
+
429
+ function clearActiveTurn(store: ContinuationStateStore, state: ContinuationDriverState, now: () => Date): ContinuationDriverState {
430
+ return saveState(store, state, {
272
431
  active_turn_step: null,
273
432
  active_turn_public_key_digest: null,
433
+ active_turn_phase: null,
434
+ active_turn_progress: null,
435
+ active_turn_capture: null,
274
436
  }, now);
275
- appendEvent(input.store, state, snapshot, "budget_exhausted", now);
276
- return { outcome: "budget_exhausted", turns_started: state.turns_started, snapshot };
437
+ }
438
+
439
+ function auditAuthorityCleanup(
440
+ store: ContinuationStateStore,
441
+ state: ContinuationDriverState,
442
+ snapshot: ContinuationSnapshot,
443
+ authority: ContinuationTurnAuthority | undefined,
444
+ now: () => Date,
445
+ ): void {
446
+ let summary: string | undefined;
447
+ try {
448
+ if (authority && !authority.cleanup()) summary = "continuation turn authority cleanup returned false";
449
+ } catch (error) {
450
+ summary = boundedErrorMessage(error);
451
+ }
452
+ if (!summary) return;
453
+ try {
454
+ appendEvent(store, state, snapshot, "authority_cleanup_failed", now, { summary });
455
+ } catch {
456
+ // Audit is best effort. A cleanup extension must not replace the adapter outcome.
457
+ }
458
+ }
459
+
460
+ function finishBudgetExhausted(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
461
+ const exhausted = saveState(store, state, {
462
+ status: "budget_exhausted", active_turn_step: null, active_turn_public_key_digest: null,
463
+ active_turn_phase: null, active_turn_progress: null, active_turn_capture: null,
464
+ }, now);
465
+ appendEvent(store, exhausted, snapshot, "budget_exhausted", now);
466
+ return { outcome: "budget_exhausted", turns_started: exhausted.turns_started, snapshot };
277
467
  }
278
468
 
279
469
  export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInput): Promise<ContinuationDriverOutcome> {
@@ -287,57 +477,16 @@ export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInpu
287
477
  maxTurns: input.maxTurns,
288
478
  ...(input.adapterCommandIdentity ? { adapterCommandIdentity: input.adapterCommandIdentity } : {}),
289
479
  runtime,
290
- store: createFileContinuationStore(sessionDir),
480
+ store: input.store ?? createFileContinuationStore(sessionDir),
291
481
  ...(input.waitForBarrier ? { waitForBarrier: input.waitForBarrier } : {}),
292
482
  ...(input.authorizeTurn ? { authorizeTurn: input.authorizeTurn } : {}),
293
483
  ...(input.issueTurnAuthority ? { issueTurnAuthority: input.issueTurnAuthority } : {}),
484
+ ...(input.preflightTurn ? { preflightTurn: input.preflightTurn } : {}),
294
485
  ...(input.onTurnAccepted ? { onTurnAccepted: input.onTurnAccepted } : {}),
295
486
  ...(input.now ? { now: input.now } : {}),
296
487
  });
297
488
  }
298
489
 
299
- export async function withContinuationDriverLock<T>(sessionDirInput: string, body: (lease: ContinuationDriverLockLease) => Promise<T>): Promise<T> {
300
- const sessionDir = path.resolve(sessionDirInput);
301
- const driverDir = path.join(sessionDir, "continuation-driver");
302
- const locksDir = path.join(driverDir, "locks");
303
- ensureSafeDirectory(sessionDir, locksDir);
304
- const token = randomUUID();
305
- const lockFile = path.join(locksDir, `${process.pid}-${token}.lock`);
306
- const lease: ContinuationDriverLockLease = { pid: process.pid, token, created_at: new Date().toISOString() };
307
- const owner = { schema_version: "1.0" as const, ...lease };
308
- acquireDriverLock(locksDir, lockFile, owner);
309
- try {
310
- return await body(lease);
311
- } finally {
312
- releaseDriverLock(lockFile, token);
313
- }
314
- }
315
-
316
- export function createFileContinuationStore(sessionDirInput: string): ContinuationStateStore {
317
- const sessionDir = path.resolve(sessionDirInput);
318
- const driverDir = path.join(sessionDir, "continuation-driver");
319
- const stateFile = path.join(driverDir, "state.json");
320
- const eventsFile = path.join(driverDir, "events.jsonl");
321
- ensureSafeDirectory(sessionDir, driverDir);
322
- return {
323
- load(): ContinuationDriverState | null {
324
- if (!fs.existsSync(stateFile)) {
325
- if (fs.existsSync(eventsFile) && fs.statSync(eventsFile).size > 0) throw new Error("continuation driver state is missing while mission events exist");
326
- return null;
327
- }
328
- const state = JSON.parse(readRegularFileNoFollow(stateFile, "continuation driver state")) as ContinuationDriverState;
329
- reconcileStateWithEvents(state, eventsFile);
330
- return state;
331
- },
332
- save(state: ContinuationDriverState): void {
333
- atomicWriteJson(sessionDir, stateFile, state);
334
- },
335
- append(event: ContinuationDriverEvent): void {
336
- appendJsonLineNoFollow(sessionDir, eventsFile, event);
337
- },
338
- };
339
- }
340
-
341
490
  function builderSessionSnapshot(result: Awaited<ReturnType<typeof inspectBuilderFlowSession>>): ContinuationSnapshot {
342
491
  const nextAction = result.projection.next_action;
343
492
  return validateSnapshot({
@@ -349,88 +498,11 @@ function builderSessionSnapshot(result: Awaited<ReturnType<typeof inspectBuilder
349
498
  next_action: nextAction && typeof nextAction === "object" && !Array.isArray(nextAction)
350
499
  ? structuredClone(nextAction as Record<string, unknown>)
351
500
  : null,
501
+ progress_snapshot: structuredClone(result.progressSnapshot),
502
+ ...(result.gateActionEnvelope ? { gate_action_envelope: structuredClone(result.gateActionEnvelope) } : {}),
352
503
  });
353
504
  }
354
505
 
355
- function appendJsonLineNoFollow(root: string, file: string, value: unknown): void {
356
- ensureSafeDirectory(root, path.dirname(file));
357
- if (fs.existsSync(file)) assertRegularFile(file, "continuation driver event log");
358
- const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
359
- const fd = fs.openSync(file, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | noFollow, 0o600);
360
- try {
361
- fs.writeFileSync(fd, `${JSON.stringify(value)}\n`, "utf8");
362
- } finally {
363
- fs.closeSync(fd);
364
- }
365
- }
366
-
367
- function assertRegularFile(file: string, label: string): void {
368
- const stat = fs.lstatSync(file);
369
- if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a regular file`);
370
- }
371
-
372
- function readRegularFileNoFollow(file: string, label: string): string {
373
- assertRegularFile(file, label);
374
- const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
375
- const fd = fs.openSync(file, fsConstants.O_RDONLY | noFollow);
376
- try {
377
- if (!fs.fstatSync(fd).isFile()) throw new Error(`${label} must be a regular file`);
378
- return fs.readFileSync(fd, "utf8");
379
- } finally {
380
- fs.closeSync(fd);
381
- }
382
- }
383
-
384
- function acquireDriverLock(
385
- locksDir: string,
386
- lockFile: string,
387
- owner: { schema_version: "1.0"; pid: number; token: string; created_at: string },
388
- ): void {
389
- const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
390
- const fd = fs.openSync(lockFile, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, 0o600);
391
- try {
392
- fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
393
- } finally {
394
- fs.closeSync(fd);
395
- }
396
- try {
397
- for (const name of fs.readdirSync(locksDir).sort()) {
398
- const candidate = path.join(locksDir, name);
399
- if (candidate === lockFile) continue;
400
- let existing: { schema_version?: unknown; pid?: unknown; token?: unknown };
401
- try {
402
- existing = JSON.parse(readRegularFileNoFollow(candidate, "continuation driver lock")) as typeof existing;
403
- } catch (error) {
404
- if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
405
- throw error;
406
- }
407
- if (existing.schema_version !== "1.0" || typeof existing.pid !== "number" || !Number.isSafeInteger(existing.pid) || existing.pid <= 0 || typeof existing.token !== "string") {
408
- throw new Error("continuation driver lock is invalid");
409
- }
410
- if (processAlive(existing.pid)) throw new Error(`continuation driver is already running under pid ${existing.pid}`);
411
- try {
412
- fs.unlinkSync(candidate);
413
- } catch (error) {
414
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
415
- }
416
- }
417
- } catch (error) {
418
- try {
419
- fs.unlinkSync(lockFile);
420
- } catch {}
421
- throw error;
422
- }
423
- }
424
-
425
- function releaseDriverLock(lockFile: string, token: string): void {
426
- try {
427
- const owner = JSON.parse(readRegularFileNoFollow(lockFile, "continuation driver lock")) as { token?: unknown };
428
- if (owner.token === token) fs.unlinkSync(lockFile);
429
- } catch (error) {
430
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
431
- }
432
- }
433
-
434
506
  function loadOrCreateState(
435
507
  store: ContinuationStateStore,
436
508
  snapshot: ContinuationSnapshot,
@@ -454,6 +526,9 @@ function loadOrCreateState(
454
526
  turns_started: 0,
455
527
  active_turn_step: null,
456
528
  active_turn_public_key_digest: null,
529
+ active_turn_phase: null,
530
+ active_turn_progress: null,
531
+ active_turn_capture: null,
457
532
  pending_barrier: null,
458
533
  updated_at: now().toISOString(),
459
534
  };
@@ -466,11 +541,146 @@ function canonicalOutcome(snapshot: ContinuationSnapshot): "done" | "waiting" |
466
541
  return snapshot.disposition;
467
542
  }
468
543
 
544
+ function reconcileProgress(
545
+ store: ContinuationStateStore,
546
+ state: ContinuationDriverState,
547
+ snapshot: ContinuationSnapshot,
548
+ now: () => Date,
549
+ ): ContinuationDriverState {
550
+ const current = progressSnapshot(snapshot);
551
+ if (!current) return state;
552
+ if (!state.last_progress) return saveState(store, state, { last_progress: current, prior_progress: initialProgress() }, now);
553
+ if (sameProgressSnapshot(state.last_progress, current)) return state;
554
+ const progress = measureProgress(state, state.last_progress, current);
555
+ return progress ? saveState(store, state, { last_progress: progress.snapshot, prior_progress: progress.delta }, now) : state;
556
+ }
557
+
558
+ function reconcileInterruptedTurn(
559
+ store: ContinuationStateStore,
560
+ state: ContinuationDriverState,
561
+ snapshot: ContinuationSnapshot,
562
+ now: () => Date,
563
+ ): ContinuationDriverState {
564
+ if (!state.active_turn_phase) return state;
565
+ if (state.active_turn_phase === "prepared") {
566
+ return clearActiveTurn(store, state, now);
567
+ }
568
+ if (state.active_turn_phase === "measured") {
569
+ if (!state.active_turn_capture) throw new Error("continuation measured turn is missing its durable capture");
570
+ const capture = state.active_turn_capture;
571
+ acceptedTurnJournal(store)?.captureAcceptedTurn(capture);
572
+ let recovered = capture.result.status === "wait"
573
+ ? saveState(store, state, { status: "waiting", pending_barrier: capture.result.barrier }, now)
574
+ : state;
575
+ appendEvent(store, recovered, snapshot, capture.result.status === "wait" ? "parked" : "turn_completed", now, {
576
+ ...(capture.result.status === "wait" ? { barrier: capture.result.barrier } : {}),
577
+ ...(capture.result.summary ? { summary: capture.result.summary } : {}),
578
+ ...(capture.progress ? { progress: capture.progress } : {}),
579
+ turn_id: capture.turn_id,
580
+ });
581
+ recovered = clearActiveTurn(store, recovered, now);
582
+ appendEvent(store, recovered, snapshot, "turn_recovered", now, {
583
+ summary: "completed durable accepted-turn capture after interruption",
584
+ ...(capture.progress ? { progress: capture.progress } : {}),
585
+ turn_id: capture.turn_id,
586
+ });
587
+ return recovered;
588
+ }
589
+ const progress = measureProgress(state, state.active_turn_progress ?? state.last_progress ?? null, progressSnapshot(snapshot));
590
+ const recovered = saveState(store, state, {
591
+ ...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
592
+ active_turn_step: null,
593
+ active_turn_public_key_digest: null,
594
+ active_turn_phase: null,
595
+ active_turn_progress: null,
596
+ active_turn_capture: null,
597
+ }, now);
598
+ appendEvent(store, recovered, snapshot, "turn_recovered", now, {
599
+ summary: "recovered interrupted adapter turn from canonical Flow progress",
600
+ ...(progress ? { progress: progress.delta } : {}),
601
+ });
602
+ return recovered;
603
+ }
604
+
605
+ function sameProgressSnapshot(left: GateActionProgressSnapshot, right: GateActionProgressSnapshot): boolean {
606
+ return left.current_step === right.current_step
607
+ && sameCanonicalStatus(left.canonical_status, right.canonical_status)
608
+ && left.canonical_evidence.length === right.canonical_evidence.length
609
+ && left.canonical_evidence.every((entry, index) => entry === right.canonical_evidence[index])
610
+ && left.observed_artifacts.length === right.observed_artifacts.length
611
+ && left.observed_artifacts.every((entry, index) => entry === right.observed_artifacts[index]);
612
+ }
613
+
614
+ function isTerminalCanonicalStatus(status: string | undefined): boolean {
615
+ return status === "completed" || status === "failed";
616
+ }
617
+
618
+ function sameCanonicalStatus(left: string | undefined, right: string | undefined): boolean {
619
+ if (left === right) return true;
620
+ // Pre-status snapshots remain compatible while active. A newly observed
621
+ // terminal status is still a real canonical transition from legacy state.
622
+ if (left === undefined) return !isTerminalCanonicalStatus(right);
623
+ if (right === undefined) return !isTerminalCanonicalStatus(left);
624
+ return false;
625
+ }
626
+
627
+ function progressSnapshot(snapshot: ContinuationSnapshot): GateActionProgressSnapshot | null {
628
+ if (snapshot.progress_snapshot) return structuredClone(snapshot.progress_snapshot);
629
+ return snapshot.gate_action_envelope ? gateActionProgressSnapshot(snapshot.gate_action_envelope) : null;
630
+ }
631
+
632
+ function acceptedTurnJournal(store: ContinuationStateStore): ContinuationAcceptedTurnJournal | null {
633
+ const candidate = store as Partial<ContinuationAcceptedTurnJournal>;
634
+ return typeof candidate.captureAcceptedTurn === "function" && typeof candidate.acceptedTurns === "function"
635
+ ? candidate as ContinuationAcceptedTurnJournal
636
+ : null;
637
+ }
638
+
639
+ function initialProgress(): GateActionPriorProgress {
640
+ return {
641
+ step_advanced: false,
642
+ evidence_added: [],
643
+ artifact_changes: [],
644
+ no_progress: false,
645
+ consecutive_no_progress: 0,
646
+ stagnation: "none",
647
+ };
648
+ }
649
+
650
+ function measureProgress(
651
+ state: ContinuationDriverState,
652
+ before: GateActionProgressSnapshot | null,
653
+ after: GateActionProgressSnapshot | null,
654
+ ): { snapshot: GateActionProgressSnapshot; delta: GateActionPriorProgress } | null {
655
+ if (!before || !after) return null;
656
+ const terminalStatusAdvanced = isTerminalCanonicalStatus(after.canonical_status)
657
+ && before.canonical_status !== after.canonical_status;
658
+ const stepAdvanced = before.current_step !== after.current_step || terminalStatusAdvanced;
659
+ const evidenceAdded = after.canonical_evidence.filter((entry) => !before.canonical_evidence.includes(entry));
660
+ const artifactChanges = after.observed_artifacts.filter((entry) => !before.observed_artifacts.includes(entry));
661
+ const noProgress = !stepAdvanced && evidenceAdded.length === 0 && artifactChanges.length === 0;
662
+ const consecutiveNoProgress = noProgress ? (state.prior_progress?.consecutive_no_progress ?? 0) + 1 : 0;
663
+ return {
664
+ snapshot: after,
665
+ delta: {
666
+ step_advanced: stepAdvanced,
667
+ evidence_added: evidenceAdded,
668
+ artifact_changes: artifactChanges,
669
+ no_progress: noProgress,
670
+ consecutive_no_progress: consecutiveNoProgress,
671
+ stagnation: consecutiveNoProgress >= 2 ? "stagnant" : consecutiveNoProgress === 1 ? "possible" : "none",
672
+ },
673
+ };
674
+ }
675
+
469
676
  function finishDone(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
470
677
  const done = saveState(store, state, {
471
678
  status: "done",
472
679
  active_turn_step: null,
473
680
  active_turn_public_key_digest: null,
681
+ active_turn_phase: null,
682
+ active_turn_progress: null,
683
+ active_turn_capture: null,
474
684
  pending_barrier: null,
475
685
  }, now);
476
686
  appendEvent(store, done, snapshot, "done", now);
@@ -482,6 +692,9 @@ function finishFailed(store: ContinuationStateStore, state: ContinuationDriverSt
482
692
  status: "failed",
483
693
  active_turn_step: null,
484
694
  active_turn_public_key_digest: null,
695
+ active_turn_phase: null,
696
+ active_turn_progress: null,
697
+ active_turn_capture: null,
485
698
  pending_barrier: null,
486
699
  }, now);
487
700
  return { outcome: "failed", turns_started: failed.turns_started, snapshot };
@@ -490,7 +703,7 @@ function finishFailed(store: ContinuationStateStore, state: ContinuationDriverSt
490
703
  function saveState(
491
704
  store: ContinuationStateStore,
492
705
  current: ContinuationDriverState,
493
- patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "active_turn_step" | "active_turn_public_key_digest" | "pending_barrier">>,
706
+ patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "active_turn_step" | "active_turn_public_key_digest" | "active_turn_phase" | "active_turn_progress" | "active_turn_capture" | "pending_barrier" | "last_progress" | "prior_progress">>,
494
707
  now: () => Date,
495
708
  ): ContinuationDriverState {
496
709
  const next = { ...current, ...patch, updated_at: now().toISOString() };
@@ -504,7 +717,7 @@ function appendEvent(
504
717
  snapshot: ContinuationSnapshot,
505
718
  type: ContinuationDriverEvent["type"],
506
719
  now: () => Date,
507
- extra: Pick<ContinuationDriverEvent, "barrier" | "summary" | "failure_kind"> = {},
720
+ extra: Pick<ContinuationDriverEvent, "barrier" | "summary" | "failure_kind" | "progress" | "turn_id"> = {},
508
721
  ): void {
509
722
  store.append({
510
723
  schema_version: "1.0",
@@ -517,141 +730,16 @@ function appendEvent(
517
730
  ...(extra.barrier ? { barrier: extra.barrier } : {}),
518
731
  ...(extra.summary ? { summary: extra.summary } : {}),
519
732
  ...(extra.failure_kind ? { failure_kind: extra.failure_kind } : {}),
733
+ ...(extra.progress ? { progress: structuredClone(extra.progress) } : {}),
734
+ ...(extra.turn_id ? { turn_id: extra.turn_id } : {}),
520
735
  });
521
736
  }
522
737
 
523
- function validateSnapshot(value: ContinuationSnapshot): ContinuationSnapshot {
524
- if (!value || typeof value !== "object") throw new Error("continuation runtime returned an invalid canonical snapshot");
525
- for (const field of ["run_id", "definition_id", "status", "current_step"] as const) {
526
- if (typeof value[field] !== "string" || value[field].length === 0) throw new Error(`continuation snapshot ${field} must be a non-empty string`);
527
- }
528
- if (!new Set(["continue", "waiting", "done", "failed"]).has(value.disposition)) throw new Error("continuation snapshot disposition is invalid");
529
- if (value.next_action !== null && (typeof value.next_action !== "object" || Array.isArray(value.next_action))) {
530
- throw new Error("continuation snapshot next_action must be an object or null");
531
- }
532
- return structuredClone(value);
533
- }
534
-
535
- function validateTurnResult(value: ContinuationTurnResult): ContinuationTurnResult {
536
- if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) {
537
- throw new Error("continuation adapter must return status completed or wait");
538
- }
539
- if (value.status === "wait") validateBarrier(value.barrier);
540
- if (value.summary !== undefined && typeof value.summary !== "string") throw new Error("continuation adapter summary must be a string");
541
- if (value.status === "completed" && value.evidence !== undefined) validateAdapterEvidence(value.evidence);
542
- if (value.status === "wait" && Object.hasOwn(value, "evidence")) throw new Error("continuation wait results must not include evidence");
543
- const copy = structuredClone(value);
544
- if (copy.summary && copy.summary.length > 2_000) copy.summary = `${copy.summary.slice(0, 1_997)}...`;
545
- return copy;
546
- }
547
-
548
- function validateAdapterEvidence(value: unknown): void {
549
- if (!value || typeof value !== "object" || Array.isArray(value)) {
550
- throw new Error("continuation adapter evidence must be a JSON object");
551
- }
552
- let encoded: string;
553
- try {
554
- encoded = JSON.stringify(value);
555
- } catch {
556
- throw new Error("continuation adapter evidence must be JSON-serializable");
557
- }
558
- if (encoded === undefined || Buffer.byteLength(encoded, "utf8") > 65_536) {
559
- throw new Error("continuation adapter evidence must not exceed 65536 bytes");
560
- }
561
- const parsed = JSON.parse(encoded) as unknown;
562
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
563
- throw new Error("continuation adapter evidence must be a JSON object");
564
- }
565
- }
566
-
567
- function validateBarrier(barrier: ContinuationBarrier): void {
568
- if (!barrier || typeof barrier !== "object") throw new Error("continuation wait result requires a barrier");
569
- if (barrier.kind === "pid") {
570
- if (!Number.isSafeInteger(barrier.pid) || barrier.pid <= 0) throw new Error("continuation pid barrier requires a positive integer pid");
571
- return;
572
- }
573
- if (barrier.kind === "deadline") {
574
- if (typeof barrier.at !== "string" || !Number.isFinite(Date.parse(barrier.at))) throw new Error("continuation deadline barrier requires an ISO date-time");
575
- return;
576
- }
577
- throw new Error("continuation barrier kind must be pid or deadline");
578
- }
579
-
580
- function validateState(state: ContinuationDriverState): void {
581
- if (state.schema_version !== "1.0") throw new Error("continuation driver state schema_version must be 1.0");
582
- assertMaxTurns(state.max_turns);
583
- if (state.adapter_command_identity !== null && (typeof state.adapter_command_identity !== "string" || state.adapter_command_identity.length === 0)) {
584
- throw new Error("continuation driver adapter_command_identity must be a non-empty string or null");
585
- }
586
- if (!Number.isSafeInteger(state.turns_started) || state.turns_started < 0) throw new Error("continuation driver turns_started must be a non-negative integer");
587
- if (state.turns_started > state.max_turns) throw new Error("continuation driver turns_started cannot exceed max_turns");
588
- if (state.active_turn_step !== undefined && state.active_turn_step !== null && (typeof state.active_turn_step !== "string" || state.active_turn_step.length === 0)) {
589
- throw new Error("continuation driver active_turn_step must be a non-empty string or null");
590
- }
591
- if (state.active_turn_public_key_digest !== undefined && state.active_turn_public_key_digest !== null
592
- && (typeof state.active_turn_public_key_digest !== "string" || !/^[a-f0-9]{64}$/.test(state.active_turn_public_key_digest))) {
593
- throw new Error("continuation driver active_turn_public_key_digest must be a SHA-256 hex digest or null");
594
- }
595
- if (!new Set(["active", "waiting", "done", "failed", "budget_exhausted"]).has(state.status)) throw new Error("continuation driver state status is invalid");
596
- if (state.status === "budget_exhausted" && state.turns_started !== state.max_turns) {
597
- throw new Error("continuation driver budget_exhausted state must consume max_turns");
598
- }
599
- if (state.pending_barrier) validateBarrier(state.pending_barrier);
600
- }
601
-
602
- function assertMissionIdentity(state: Pick<ContinuationDriverState, "run_id" | "definition_id">, snapshot: ContinuationSnapshot): void {
603
- if (state.run_id !== snapshot.run_id || state.definition_id !== snapshot.definition_id) {
604
- throw new Error("continuation driver mission identity does not match the canonical Flow run");
605
- }
606
- }
607
-
608
- function assertMaxTurns(value: number): void {
609
- if (!Number.isSafeInteger(value) || value < 1 || value > 100) throw new Error("continuation maxTurns must be an integer from 1 through 100");
610
- }
611
-
612
738
  function boundedErrorMessage(error: unknown): string {
613
739
  const message = error instanceof Error ? error.message : String(error);
614
740
  return message.length <= 2_000 ? message : `${message.slice(0, 1_997)}...`;
615
741
  }
616
742
 
617
- function processAlive(pid: number): boolean {
618
- try {
619
- process.kill(pid, 0);
620
- return true;
621
- } catch (error) {
622
- return (error as NodeJS.ErrnoException).code === "EPERM";
623
- }
624
- }
625
-
626
- function reconcileStateWithEvents(state: ContinuationDriverState, eventsFile: string): void {
627
- if (!fs.existsSync(eventsFile)) {
628
- if (state.turns_started > 0) throw new Error("continuation driver event history is missing for a started mission");
629
- return;
630
- }
631
- const lines = readRegularFileNoFollow(eventsFile, "continuation driver event log").split("\n").filter((line) => line.length > 0);
632
- let highestStartedTurn = 0;
633
- let eventBarrier: ContinuationBarrier | null = null;
634
- for (const line of lines) {
635
- const event = JSON.parse(line) as Partial<ContinuationDriverEvent>;
636
- if (event.run_id !== state.run_id || event.definition_id !== state.definition_id) throw new Error("continuation driver event history has a foreign mission identity");
637
- if (event.type === "turn_started") {
638
- if (!Number.isSafeInteger(event.turns_started) || (event.turns_started as number) < 1) throw new Error("continuation driver event history has an invalid turn count");
639
- highestStartedTurn = Math.max(highestStartedTurn, event.turns_started as number);
640
- }
641
- if (event.type === "parked") {
642
- validateBarrier(event.barrier as ContinuationBarrier);
643
- eventBarrier = structuredClone(event.barrier as ContinuationBarrier);
644
- }
645
- if (event.type === "resumed") eventBarrier = null;
646
- }
647
- if (highestStartedTurn > state.turns_started) throw new Error("continuation driver state rolled back behind its event history");
648
- if (state.turns_started > highestStartedTurn + 1) throw new Error("continuation driver state is ahead of its event history");
649
- if (!state.pending_barrier && eventBarrier) {
650
- state.pending_barrier = eventBarrier;
651
- state.status = "waiting";
652
- }
653
- }
654
-
655
743
  function builderContinuationDisposition(nextAction: unknown): ContinuationSnapshot["disposition"] {
656
744
  if (!nextAction || typeof nextAction !== "object" || Array.isArray(nextAction)) throw new Error("Builder Flow projection is missing its canonical next action");
657
745
  const status = (nextAction as { status?: unknown }).status;