@deksden-com/dd-flow-cli 0.9.0-beta.16 → 0.9.0-beta.161

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 (670) hide show
  1. package/CHANGELOG.md +965 -0
  2. package/README.md +139 -10
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +417 -0
  6. package/dist/cli/command-metadata.js +303 -0
  7. package/dist/cli/command-option-descriptions.js +192 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/delivery.js +108 -0
  11. package/dist/cli/diagnostic-invocation.js +62 -0
  12. package/dist/cli/diagnostic.js +131 -0
  13. package/dist/cli/error-output.js +62 -0
  14. package/dist/cli/executor-onboarding.js +76 -0
  15. package/dist/cli/flows.js +29 -0
  16. package/dist/cli/harnesses.js +84 -0
  17. package/dist/cli/help.js +521 -19
  18. package/dist/cli/hook-ingress.js +89 -0
  19. package/dist/cli/input-preparation.js +193 -0
  20. package/dist/cli/interactive-invocation.js +33 -0
  21. package/dist/cli/interactive.js +114 -0
  22. package/dist/cli/memory-fix-invocation.js +52 -0
  23. package/dist/cli/memory-fix.js +100 -0
  24. package/dist/cli/memory-flow-admission.js +54 -0
  25. package/dist/cli/memory-init-invocation.js +85 -0
  26. package/dist/cli/memory-init.js +250 -0
  27. package/dist/cli/memory-storage-admission.js +54 -0
  28. package/dist/cli/memory-storage-qualification.js +105 -0
  29. package/dist/cli/memory-storage-transition.js +123 -0
  30. package/dist/cli/memory-upgrade.js +189 -0
  31. package/dist/cli/models.js +39 -0
  32. package/dist/cli/native-hook-ingress.js +94 -0
  33. package/dist/cli/native-hook-preparation.js +82 -0
  34. package/dist/cli/operator-profiles.js +61 -0
  35. package/dist/cli/output-options.js +35 -0
  36. package/dist/cli/response-file.js +72 -0
  37. package/dist/cli/run-cli.js +2020 -555
  38. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  39. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  40. package/dist/cli/runtime-process.js +153 -0
  41. package/dist/cli/setup.js +150 -0
  42. package/dist/cli.js +103 -2
  43. package/dist/domain/delivery-flows.js +8 -0
  44. package/dist/domain/diagnostic-aspects.js +103 -0
  45. package/dist/domain/diagnostic-flows.js +8 -0
  46. package/dist/domain/distill-flow.js +20 -0
  47. package/dist/domain/interactive-flow.js +1 -0
  48. package/dist/domain/memory-fix-flow.js +2 -0
  49. package/dist/domain/review-fix-flow.js +2 -0
  50. package/dist/domain/source-snapshot-flows.js +4 -0
  51. package/dist/domain/stage-catalog.js +1 -1
  52. package/dist/domain/stage-handler-identities.js +28 -0
  53. package/dist/harness-runtime/DD-ZCODE.md +112 -0
  54. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  55. package/dist/harness-runtime/bin/dd-agy.mjs +71 -0
  56. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  57. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  58. package/dist/harness-runtime/bin/dd-grok.mjs +50 -0
  59. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  60. package/dist/harness-runtime/bin/dd-zcode.mjs +95 -0
  61. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  62. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  63. package/dist/harness-runtime/lib/agy-error-boundary.mjs +87 -0
  64. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  65. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  66. package/dist/harness-runtime/lib/agy-native-inspection.mjs +225 -0
  67. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  68. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  69. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +74 -0
  70. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  71. package/dist/harness-runtime/lib/daemon-observation.d.mts +3 -0
  72. package/dist/harness-runtime/lib/daemon-observation.mjs +171 -0
  73. package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
  74. package/dist/harness-runtime/lib/daemon-operations.mjs +512 -0
  75. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +7 -0
  76. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1362 -0
  77. package/dist/harness-runtime/lib/dd-agy.mjs +105 -0
  78. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  79. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +271 -0
  80. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  81. package/dist/harness-runtime/lib/dd-codex.mjs +986 -0
  82. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +155 -0
  83. package/dist/harness-runtime/lib/dd-droid.mjs +739 -0
  84. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  85. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +667 -0
  86. package/dist/harness-runtime/lib/dd-grok.mjs +182 -0
  87. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +772 -0
  88. package/dist/harness-runtime/lib/dd-opencode.mjs +411 -0
  89. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  90. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +845 -0
  91. package/dist/harness-runtime/lib/dd-zcode.mjs +1239 -0
  92. package/dist/harness-runtime/lib/delegation-instructions.d.mts +16 -0
  93. package/dist/harness-runtime/lib/delegation-instructions.mjs +179 -0
  94. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  95. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  96. package/dist/harness-runtime/lib/driver-recovery.mjs +121 -0
  97. package/dist/harness-runtime/lib/droid-observation.mjs +70 -0
  98. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  99. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  100. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  101. package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
  102. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  103. package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
  104. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +11 -0
  105. package/dist/harness-runtime/lib/lifecycle-contract.mjs +20 -0
  106. package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
  107. package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
  108. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  109. package/dist/harness-runtime/lib/model-observations.mjs +113 -0
  110. package/dist/harness-runtime/lib/native-children.d.mts +34 -0
  111. package/dist/harness-runtime/lib/native-children.mjs +265 -0
  112. package/dist/harness-runtime/lib/native-hook-command.d.mts +8 -0
  113. package/dist/harness-runtime/lib/native-hook-command.mjs +306 -0
  114. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  115. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  116. package/dist/harness-runtime/lib/native-streams.mjs +38 -0
  117. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  118. package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
  119. package/dist/harness-runtime/lib/opencode-plugin.mjs +112 -0
  120. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  121. package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
  122. package/dist/harness-runtime/lib/operation-errors.mjs +140 -0
  123. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  124. package/dist/harness-runtime/lib/operation-progress.mjs +118 -0
  125. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  126. package/dist/harness-runtime/lib/process-json.mjs +200 -0
  127. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  128. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +23 -0
  129. package/dist/harness-runtime/lib/provider-overload-policy.mjs +242 -0
  130. package/dist/harness-runtime/lib/runner-events.mjs +202 -0
  131. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  132. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  133. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  134. package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
  135. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  136. package/dist/harness-runtime/lib/tool-observations.mjs +238 -0
  137. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  138. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  139. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  140. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  141. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  142. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  143. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  144. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  145. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  146. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  147. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  148. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  149. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  150. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  151. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  152. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  153. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  154. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  155. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  156. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  157. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  158. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  159. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  160. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  161. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  162. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  163. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  164. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  165. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  166. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  167. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  168. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  169. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  170. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  171. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  172. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  173. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  174. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  175. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  176. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  177. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  178. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  179. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  180. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  181. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  182. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  183. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  184. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  185. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  186. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  187. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  188. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  189. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  190. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  191. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  192. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  193. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  194. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  195. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  196. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  197. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  198. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  199. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  200. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  201. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  202. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  203. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  204. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  205. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  206. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  207. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  208. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  209. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  210. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  211. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  212. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  213. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  214. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  215. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  216. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  217. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  218. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  219. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  220. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  221. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  222. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  223. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  224. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  225. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  226. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  227. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  228. package/dist/protocol/local-files.js +18 -9
  229. package/dist/runtime/context.js +8 -4
  230. package/dist/schemas/agent-profile.schema.json +1 -1
  231. package/dist/schemas/code-review-decision.schema.json +9 -0
  232. package/dist/schemas/code-review-result.schema.json +1 -1
  233. package/dist/schemas/code-verification.schema.json +7 -5
  234. package/dist/schemas/code-work-batch.schema.json +2 -2
  235. package/dist/schemas/code-work-result.schema.json +2 -1
  236. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  237. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  238. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  239. package/dist/schemas/discovery-result.schema.json +184 -0
  240. package/dist/schemas/distill-stage-result.schema.json +667 -0
  241. package/dist/schemas/distill-work-result.schema.json +428 -0
  242. package/dist/schemas/execution-bundle.schema.json +215 -0
  243. package/dist/schemas/execution-settings.schema.json +162 -0
  244. package/dist/schemas/flow-descriptor.schema.json +54 -0
  245. package/dist/schemas/flow-run.schema.json +29 -0
  246. package/dist/schemas/harness-catalog.schema.json +80 -0
  247. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  248. package/dist/schemas/interactive-work-result.schema.json +63 -0
  249. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  250. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  251. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  252. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  253. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  254. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  255. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  256. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  257. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  258. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  259. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  260. package/dist/schemas/memory-repair-work-result.schema.json +56 -0
  261. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  262. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  263. package/dist/schemas/model-catalog.schema.json +153 -0
  264. package/dist/schemas/plan-review-decision.schema.json +12 -3
  265. package/dist/schemas/plan-review-result.schema.json +2 -2
  266. package/dist/schemas/profile-settings.schema.json +122 -0
  267. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  268. package/dist/schemas/release-impact.schema.json +3 -3
  269. package/dist/schemas/release-manifest.schema.json +291 -0
  270. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  271. package/dist/schemas/run-control-receipt.schema.json +119 -0
  272. package/dist/schemas/run-control-request.schema.json +36 -0
  273. package/dist/schemas/run-engine-binding.schema.json +1 -1
  274. package/dist/schemas/stage-start-response.schema.json +4 -0
  275. package/dist/schemas/vnext-protocol-plan.schema.json +3 -3
  276. package/dist/services/accepted-intent.js +139 -0
  277. package/dist/services/assigned-lifecycle.js +382 -0
  278. package/dist/services/branch-context.js +3 -3
  279. package/dist/services/canon.js +54 -15
  280. package/dist/services/check-environment.js +21 -0
  281. package/dist/services/check-runner-evidence.js +344 -0
  282. package/dist/services/check-runner-proof.js +373 -0
  283. package/dist/services/cleanup.js +133 -40
  284. package/dist/services/cli-operation-classifier.js +32 -64
  285. package/dist/services/cli-upgrade-allowlist.js +27 -0
  286. package/dist/services/code-checks.js +1426 -121
  287. package/dist/services/codex-capacity.js +367 -0
  288. package/dist/services/codex-hook-delivery.js +41 -0
  289. package/dist/services/command-context.js +80 -0
  290. package/dist/services/compatibility-preflight.js +16 -3
  291. package/dist/services/config.js +11 -8
  292. package/dist/services/continuation-outcome.js +25 -0
  293. package/dist/services/controller-child-capacity.js +71 -0
  294. package/dist/services/controller-child-observer.js +121 -0
  295. package/dist/services/controller-child-repair.js +117 -0
  296. package/dist/services/controller-execution.js +97 -0
  297. package/dist/services/controller-fanout.js +277 -0
  298. package/dist/services/controller-memory-task.js +45 -0
  299. package/dist/services/controller-memory-worker.js +51 -0
  300. package/dist/services/controller-result-authority.js +127 -0
  301. package/dist/services/controller-work-failure.js +198 -0
  302. package/dist/services/dashboard.js +145 -58
  303. package/dist/services/delivery-access.js +123 -0
  304. package/dist/services/delivery-approval.js +68 -0
  305. package/dist/services/delivery-artifact.js +86 -0
  306. package/dist/services/delivery-checks.js +178 -0
  307. package/dist/services/delivery-execution.js +181 -0
  308. package/dist/services/delivery-intents.js +212 -0
  309. package/dist/services/delivery-operation.js +81 -0
  310. package/dist/services/delivery-policy.js +91 -0
  311. package/dist/services/delivery-procedures.js +43 -0
  312. package/dist/services/delivery-request.js +99 -0
  313. package/dist/services/delivery-result.js +144 -0
  314. package/dist/services/delivery-stage-commands.js +26 -0
  315. package/dist/services/delivery-stage-entry.js +56 -0
  316. package/dist/services/delivery-stage-handlers.js +135 -0
  317. package/dist/services/delivery-stage-results.js +94 -0
  318. package/dist/services/descriptor-coordinator-session.js +37 -0
  319. package/dist/services/descriptor-root-work.js +53 -0
  320. package/dist/services/descriptor-run-settlement.js +107 -0
  321. package/dist/services/descriptor-run-start.js +495 -0
  322. package/dist/services/descriptor-stage-coordinator.js +318 -0
  323. package/dist/services/descriptor-stage-settlement.js +268 -0
  324. package/dist/services/descriptor-stage-start.js +175 -0
  325. package/dist/services/diagnostic-evidence.js +73 -0
  326. package/dist/services/diagnostic-repair-intake.js +123 -0
  327. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  328. package/dist/services/diagnostic-report-publication.js +72 -0
  329. package/dist/services/diagnostic-report.js +97 -0
  330. package/dist/services/diagnostic-run-bundle.js +113 -0
  331. package/dist/services/diagnostic-snapshot.js +159 -0
  332. package/dist/services/diagnostic-stage-handlers.js +109 -0
  333. package/dist/services/diagnostic-stage-results.js +158 -0
  334. package/dist/services/diagnostic-start.js +134 -0
  335. package/dist/services/diagnostic-work-batch.js +34 -0
  336. package/dist/services/diagnostic-work-collection.js +97 -0
  337. package/dist/services/diagnostic-work-registration.js +101 -0
  338. package/dist/services/diagnostic-work-results.js +93 -0
  339. package/dist/services/discovery/agy-discovery.js +2 -0
  340. package/dist/services/discovery/codex-discovery.js +2 -0
  341. package/dist/services/discovery/droid-discovery.js +2 -0
  342. package/dist/services/discovery/grok-discovery.js +2 -0
  343. package/dist/services/discovery/opencode-discovery.js +2 -0
  344. package/dist/services/discovery/static.js +31 -0
  345. package/dist/services/discovery/zcode-discovery.js +2 -0
  346. package/dist/services/distill-document-publication.js +280 -0
  347. package/dist/services/distill-promotion.js +197 -0
  348. package/dist/services/distill-publication-custody.js +162 -0
  349. package/dist/services/distill-stage-handlers.js +249 -0
  350. package/dist/services/distill-stage-results.js +105 -0
  351. package/dist/services/distill-user-inputs.js +4 -0
  352. package/dist/services/distill-work-batch.js +42 -0
  353. package/dist/services/distill-work-collection.js +74 -0
  354. package/dist/services/distill-work-registration.js +93 -0
  355. package/dist/services/distill-work-results.js +98 -0
  356. package/dist/services/engines.js +354 -163
  357. package/dist/services/eval-snapshots.js +1326 -150
  358. package/dist/services/execution-bundles.js +275 -0
  359. package/dist/services/execution-policy.js +185 -0
  360. package/dist/services/executor-onboarding.js +56 -0
  361. package/dist/services/external-work-launch.js +316 -0
  362. package/dist/services/flow-catalog.js +54 -0
  363. package/dist/services/flow-descriptors.js +82 -0
  364. package/dist/services/frozen-engine.js +404 -0
  365. package/dist/services/generic-execution.js +165 -0
  366. package/dist/services/harness-adapter.js +342 -35
  367. package/dist/services/harness-config.js +2 -0
  368. package/dist/services/harness-discovery.js +29 -0
  369. package/dist/services/harness-doctor.js +75 -0
  370. package/dist/services/harness-probe-effects.js +51 -0
  371. package/dist/services/harness-version-probe.js +100 -0
  372. package/dist/services/hook-memory-routing.js +188 -0
  373. package/dist/services/hook-recovery-guard.js +14 -0
  374. package/dist/services/hook-session-observation.js +24 -0
  375. package/dist/services/hooks.js +813 -327
  376. package/dist/services/hybrid-contracts.js +479 -0
  377. package/dist/services/hybrid-digest.js +14 -0
  378. package/dist/services/ids.js +29 -1
  379. package/dist/services/installation-lock.js +95 -0
  380. package/dist/services/installed-flow-source.js +261 -0
  381. package/dist/services/interactive-attached-session.js +215 -0
  382. package/dist/services/interactive-baseline-custody.js +54 -0
  383. package/dist/services/interactive-before-versions.js +148 -0
  384. package/dist/services/interactive-bundle-data.js +85 -0
  385. package/dist/services/interactive-checks.js +179 -0
  386. package/dist/services/interactive-execution.js +33 -0
  387. package/dist/services/interactive-finish-integration.js +236 -0
  388. package/dist/services/interactive-git-baseline.js +230 -0
  389. package/dist/services/interactive-git-dispatch.js +278 -0
  390. package/dist/services/interactive-input-delivery.js +82 -0
  391. package/dist/services/interactive-mutation-scope.js +231 -0
  392. package/dist/services/interactive-request.js +287 -0
  393. package/dist/services/interactive-run-bundle.js +109 -0
  394. package/dist/services/interactive-stage-entry.js +41 -0
  395. package/dist/services/interactive-stage-handlers.js +289 -0
  396. package/dist/services/interactive-stage-observation.js +136 -0
  397. package/dist/services/interactive-stage-results.js +111 -0
  398. package/dist/services/interactive-user-inputs.js +142 -0
  399. package/dist/services/interactive-work-batch.js +43 -0
  400. package/dist/services/interactive-work-collection.js +119 -0
  401. package/dist/services/interactive-work-completion.js +180 -0
  402. package/dist/services/interactive-work-context.js +249 -0
  403. package/dist/services/interactive-work-history.js +112 -0
  404. package/dist/services/interactive-work-registration.js +124 -0
  405. package/dist/services/interactive-work-results.js +115 -0
  406. package/dist/services/interactive-work-start.js +155 -0
  407. package/dist/services/lanes.js +78 -65
  408. package/dist/services/lifecycle-command.js +1 -288
  409. package/dist/services/lifecycle-contract.js +1 -0
  410. package/dist/services/lifecycle-invocations.js +2549 -0
  411. package/dist/services/managed-daemon-binding.js +129 -0
  412. package/dist/services/managed-lease-monitor.js +133 -0
  413. package/dist/services/managed-processes.js +449 -69
  414. package/dist/services/memory-accepted-composition.js +78 -0
  415. package/dist/services/memory-baseline-preservation.js +65 -0
  416. package/dist/services/memory-completed-correction-wave.js +328 -0
  417. package/dist/services/memory-compose-handler.js +380 -0
  418. package/dist/services/memory-composition-acceptance.js +375 -0
  419. package/dist/services/memory-composition-graph.js +165 -0
  420. package/dist/services/memory-composition-registration.js +161 -0
  421. package/dist/services/memory-composition-work-custody.js +156 -0
  422. package/dist/services/memory-composition-work-results.js +108 -0
  423. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  424. package/dist/services/memory-corrected-review-batch.js +37 -0
  425. package/dist/services/memory-corrected-review-findings.js +108 -0
  426. package/dist/services/memory-corrected-review-inputs.js +215 -0
  427. package/dist/services/memory-corrected-review-projection.js +78 -0
  428. package/dist/services/memory-corrected-review-receipt.js +178 -0
  429. package/dist/services/memory-corrected-review-registration.js +192 -0
  430. package/dist/services/memory-correction-check-receipt.js +235 -0
  431. package/dist/services/memory-correction-input-owner.js +6 -0
  432. package/dist/services/memory-custody-snapshot.js +36 -0
  433. package/dist/services/memory-discover-handler.js +104 -0
  434. package/dist/services/memory-finish-evidence.js +457 -0
  435. package/dist/services/memory-finish-handler.js +229 -0
  436. package/dist/services/memory-finish-integration.js +491 -0
  437. package/dist/services/memory-finish-readiness.js +169 -0
  438. package/dist/services/memory-fix-finish-integration.js +129 -0
  439. package/dist/services/memory-fix-git-dispatch.js +139 -0
  440. package/dist/services/memory-fix-intake-custody.js +11 -0
  441. package/dist/services/memory-fix-intake-settlement.js +127 -0
  442. package/dist/services/memory-fix-publication.js +196 -0
  443. package/dist/services/memory-fix-repair-admission.js +116 -0
  444. package/dist/services/memory-fix-request.js +141 -0
  445. package/dist/services/memory-fix-run-bundle.js +83 -0
  446. package/dist/services/memory-fix-scope.js +78 -0
  447. package/dist/services/memory-fix-sources.js +85 -0
  448. package/dist/services/memory-fix-stage-entry.js +24 -0
  449. package/dist/services/memory-fix-stage-handlers.js +282 -0
  450. package/dist/services/memory-fix-stage-results.js +106 -0
  451. package/dist/services/memory-fix-workspace.js +127 -0
  452. package/dist/services/memory-flow-user-inputs.js +88 -0
  453. package/dist/services/memory-git-integration.js +220 -0
  454. package/dist/services/memory-historical-corrected-review.js +212 -0
  455. package/dist/services/memory-historical-original-review.js +143 -0
  456. package/dist/services/memory-historical-repair-registration.js +105 -0
  457. package/dist/services/memory-historical-second-review.js +147 -0
  458. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  459. package/dist/services/memory-init-contexts.js +181 -0
  460. package/dist/services/memory-init-controller-start.js +119 -0
  461. package/dist/services/memory-init-engine-release.js +223 -0
  462. package/dist/services/memory-init-git-policy.js +165 -0
  463. package/dist/services/memory-init-lint-capability.js +203 -0
  464. package/dist/services/memory-init-native-capacity.js +309 -0
  465. package/dist/services/memory-init-preflight.js +334 -0
  466. package/dist/services/memory-init-project-bootstrap.js +599 -0
  467. package/dist/services/memory-init-request-candidate.js +213 -0
  468. package/dist/services/memory-init-request-input.js +82 -0
  469. package/dist/services/memory-init-resource-storage.js +125 -0
  470. package/dist/services/memory-init-run-allocation.js +81 -0
  471. package/dist/services/memory-init-run-bundle.js +135 -0
  472. package/dist/services/memory-init-start.js +181 -0
  473. package/dist/services/memory-init-startup-custody.js +80 -0
  474. package/dist/services/memory-init-startup-input.js +240 -0
  475. package/dist/services/memory-init-storage.js +91 -0
  476. package/dist/services/memory-init-stores.js +359 -0
  477. package/dist/services/memory-init-worktrunk-capability.js +174 -0
  478. package/dist/services/memory-layer-publication.js +611 -0
  479. package/dist/services/memory-layer.js +225 -0
  480. package/dist/services/memory-lint-config.js +194 -0
  481. package/dist/services/memory-native-review-copy.js +127 -0
  482. package/dist/services/memory-normal-flow-entry.js +87 -0
  483. package/dist/services/memory-ordinary-defaults.js +129 -0
  484. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  485. package/dist/services/memory-preflight-handler.js +274 -0
  486. package/dist/services/memory-repair-current-view.js +299 -0
  487. package/dist/services/memory-repair-document-delta.js +489 -0
  488. package/dist/services/memory-repair-history.js +496 -0
  489. package/dist/services/memory-repair-physical-binding.js +194 -0
  490. package/dist/services/memory-repair-publication-files.js +152 -0
  491. package/dist/services/memory-repair-publication-intent.js +27 -0
  492. package/dist/services/memory-repair-publication-plan.js +162 -0
  493. package/dist/services/memory-repair-publication-source.js +225 -0
  494. package/dist/services/memory-repair-publication.js +795 -0
  495. package/dist/services/memory-repair-registration.js +240 -0
  496. package/dist/services/memory-repair-work-baseline.js +415 -0
  497. package/dist/services/memory-repair-work-completion.js +117 -0
  498. package/dist/services/memory-repair-work-custody.js +73 -0
  499. package/dist/services/memory-repair-work-results.js +108 -0
  500. package/dist/services/memory-repair-workspace.js +261 -0
  501. package/dist/services/memory-repair-writer-copy.js +736 -0
  502. package/dist/services/memory-result-binding.js +239 -0
  503. package/dist/services/memory-retained-review-authority.js +138 -0
  504. package/dist/services/memory-retained-review-results.js +70 -0
  505. package/dist/services/memory-retained-second-baseline.js +102 -0
  506. package/dist/services/memory-retained-second-repairs.js +162 -0
  507. package/dist/services/memory-review-acceptance.js +214 -0
  508. package/dist/services/memory-review-correction-plan.js +206 -0
  509. package/dist/services/memory-review-handler.js +585 -0
  510. package/dist/services/memory-review-inputs.js +246 -0
  511. package/dist/services/memory-review-limits.js +10 -0
  512. package/dist/services/memory-review-receipt.js +180 -0
  513. package/dist/services/memory-review-registration.js +117 -0
  514. package/dist/services/memory-review-work-custody.js +126 -0
  515. package/dist/services/memory-review-work-input.js +91 -0
  516. package/dist/services/memory-review-work-results.js +95 -0
  517. package/dist/services/memory-second-corrected-review-inputs.js +167 -0
  518. package/dist/services/memory-second-correction-plan.js +77 -0
  519. package/dist/services/memory-second-repair-baseline.js +91 -0
  520. package/dist/services/memory-second-repair-launch.js +614 -0
  521. package/dist/services/memory-second-repair-registration.js +88 -0
  522. package/dist/services/memory-second-repair-source.js +187 -0
  523. package/dist/services/memory-second-review-acceptance.js +115 -0
  524. package/dist/services/memory-source-observations.js +117 -0
  525. package/dist/services/memory-stage-dispatch.js +65 -0
  526. package/dist/services/memory-stage-results.js +290 -0
  527. package/dist/services/memory-tool-probe.js +25 -0
  528. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  529. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  530. package/dist/services/memory-upgrade-evidence.js +100 -0
  531. package/dist/services/memory-upgrade-inventory.js +442 -0
  532. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  533. package/dist/services/memory-upgrade-publication.js +413 -0
  534. package/dist/services/memory-upgrade-selection.js +430 -0
  535. package/dist/services/memory-upgrade-start.js +60 -0
  536. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  537. package/dist/services/memory-verification-acceptance.js +156 -0
  538. package/dist/services/memory-verification-checks.js +317 -0
  539. package/dist/services/memory-verification-receipt.js +160 -0
  540. package/dist/services/memory-verify-handler.js +137 -0
  541. package/dist/services/memory-work-result-json.js +12 -0
  542. package/dist/services/memory-work-result-preparation.js +64 -0
  543. package/dist/services/merge-queue.js +228 -149
  544. package/dist/services/merge-server.js +163 -35
  545. package/dist/services/migrations.js +164 -114
  546. package/dist/services/model-catalog.js +109 -0
  547. package/dist/services/model-discovery.js +46 -0
  548. package/dist/services/native-daemon-history.js +50 -0
  549. package/dist/services/native-session-control.js +39 -0
  550. package/dist/services/operator-profiles.js +236 -0
  551. package/dist/services/physical-startup.js +45 -0
  552. package/dist/services/plan-runtime.js +26 -11
  553. package/dist/services/plans.js +28 -25
  554. package/dist/services/project-flow-source.js +195 -0
  555. package/dist/services/project-git-integration.js +92 -0
  556. package/dist/services/project-registry.js +97 -0
  557. package/dist/services/project-summary.js +1 -1
  558. package/dist/services/projects.js +17 -87
  559. package/dist/services/prompt-delivery.js +71 -0
  560. package/dist/services/prompts.js +28 -17
  561. package/dist/services/protocols.js +163 -87
  562. package/dist/services/provider-capacity.js +463 -0
  563. package/dist/services/recovery-observation-budget.js +121 -0
  564. package/dist/services/recovery-snapshot-database.js +588 -0
  565. package/dist/services/release-archive-producer.js +52 -0
  566. package/dist/services/release-archive.js +143 -0
  567. package/dist/services/release-cache.js +150 -0
  568. package/dist/services/release-compatibility.js +18 -0
  569. package/dist/services/release-download.js +67 -0
  570. package/dist/services/release-envelope.js +104 -0
  571. package/dist/services/release-manifest.js +45 -0
  572. package/dist/services/release-payload.js +114 -0
  573. package/dist/services/release-producer.js +63 -0
  574. package/dist/services/release-provenance.js +160 -0
  575. package/dist/services/release-registry.js +66 -0
  576. package/dist/services/release-source.js +194 -0
  577. package/dist/services/repair-intents.js +29 -0
  578. package/dist/services/repair-receipt.js +13 -0
  579. package/dist/services/retained-native-hook.js +274 -0
  580. package/dist/services/review-copy.js +165 -0
  581. package/dist/services/review-deferral.js +23 -0
  582. package/dist/services/review-fix-handoff.js +82 -0
  583. package/dist/services/review-fix-intake-custody.js +17 -0
  584. package/dist/services/review-fix-stage-entry.js +43 -0
  585. package/dist/services/review-fix-stage-handlers.js +176 -0
  586. package/dist/services/review-fix-stage-results.js +86 -0
  587. package/dist/services/run-control-receipt.js +56 -0
  588. package/dist/services/run-control-worker.js +469 -0
  589. package/dist/services/run-control.js +1109 -0
  590. package/dist/services/run-controller-adapter.js +434 -0
  591. package/dist/services/run-controller-capture.js +186 -0
  592. package/dist/services/run-controller-process.js +226 -0
  593. package/dist/services/run-controller-recovery.js +335 -0
  594. package/dist/services/run-controller-state.js +106 -0
  595. package/dist/services/run-controller.js +1717 -0
  596. package/dist/services/run-engine-bindings.js +1 -1
  597. package/dist/services/run-fork.js +147 -0
  598. package/dist/services/run-health.js +149 -0
  599. package/dist/services/run-observations.js +152 -0
  600. package/dist/services/run-projection.js +22 -64
  601. package/dist/services/run-recovery-guard.js +107 -0
  602. package/dist/services/run-recovery-runtime.js +112 -0
  603. package/dist/services/run-recovery.js +485 -0
  604. package/dist/services/run-start-custody.js +189 -0
  605. package/dist/services/run-timeline.js +59 -0
  606. package/dist/services/runs.js +996 -271
  607. package/dist/services/runtime-budget.js +446 -0
  608. package/dist/services/runtime-command.js +43 -0
  609. package/dist/services/runtime-scope-capture.js +52 -0
  610. package/dist/services/runtime-scope-control.js +468 -0
  611. package/dist/services/runtime-scope-resume.js +605 -0
  612. package/dist/services/runtime-scope-stop.js +211 -0
  613. package/dist/services/runtime-scope-worker.js +314 -0
  614. package/dist/services/runtime-service.js +19 -14
  615. package/dist/services/schema-validation.js +10 -8
  616. package/dist/services/scope-writer-cleanup.js +27 -0
  617. package/dist/services/sessions.js +37 -91
  618. package/dist/services/setup.js +427 -0
  619. package/dist/services/stage-blocker.js +17 -7
  620. package/dist/services/stage-context.js +240 -18
  621. package/dist/services/stage-handlers.js +141 -0
  622. package/dist/services/stage-lifecycle.js +156 -119
  623. package/dist/services/stage-pause.js +222 -94
  624. package/dist/services/stage-report-renderer.js +230 -6
  625. package/dist/services/stage-work-graph.js +52 -0
  626. package/dist/services/technical-notes.js +31 -0
  627. package/dist/services/tooling.js +111 -44
  628. package/dist/services/usage.js +49 -101
  629. package/dist/services/verification-matrix.js +393 -0
  630. package/dist/services/version-status.js +1 -1
  631. package/dist/services/vnext-code-review.js +302 -91
  632. package/dist/services/vnext-code.js +325 -251
  633. package/dist/services/vnext-contracts.js +10 -1
  634. package/dist/services/vnext-execution-profile.js +80 -5
  635. package/dist/services/vnext-fanout.js +399 -20
  636. package/dist/services/vnext-merge.js +336 -114
  637. package/dist/services/vnext-plan-review.js +288 -133
  638. package/dist/services/vnext-plan.js +257 -110
  639. package/dist/services/vnext-protocolize.js +534 -96
  640. package/dist/services/vnext-specify.js +131 -84
  641. package/dist/services/vnext-workspace-policy.js +7 -2
  642. package/dist/services/work-registry.js +1950 -215
  643. package/dist/services/work-retry-context.js +55 -0
  644. package/dist/services/workspace-bootstrap.js +53 -15
  645. package/dist/services/workspace-files.js +85 -0
  646. package/dist/services/worktrees.js +108 -48
  647. package/dist/shared/entity-references.js +1 -0
  648. package/dist/shared/errors.js +27 -0
  649. package/dist/storage/agent-output.js +26 -0
  650. package/dist/storage/data-preservation.js +65 -0
  651. package/dist/storage/database.js +931 -40
  652. package/dist/storage/foreign-key-transaction.js +39 -0
  653. package/dist/storage/immutable-input.js +45 -0
  654. package/dist/storage/import-guard.js +159 -0
  655. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  656. package/dist/storage/native-cleanup.js +52 -0
  657. package/dist/storage/paired-writer-migration.js +424 -0
  658. package/dist/storage/paths.js +17 -4
  659. package/dist/storage/projection-lock.js +73 -0
  660. package/dist/storage/work-references.js +23 -0
  661. package/dist/storage/writer-contract.js +96 -0
  662. package/dist/storage/writer-migration.js +346 -0
  663. package/dist/storage/writer-restore.js +217 -0
  664. package/dist/verification-components.json +384 -0
  665. package/package.json +52 -13
  666. package/tools/create-canon-release.mjs +33 -0
  667. package/tools/probe-codex-history.mjs +37 -0
  668. package/tools/produce-hybrid-release.mjs +41 -0
  669. package/tools/repair-paused-run-status.mjs +84 -0
  670. package/tools/verification-jobs.mjs +342 -0
@@ -0,0 +1,1717 @@
1
+ import { assertLifecycleOutcomes, currentLifecycleFailure, currentStageFinishAuthority, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
2
+ import { stageAttempt } from "./stage-work-graph.js";
3
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
4
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
5
+ export function recoveryPacketMatchesStage(packet, scope) {
6
+ return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
7
+ && packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
8
+ && packet.work_id === scope.workId && packet.paused === false;
9
+ }
10
+ import crypto from "node:crypto";
11
+ import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { setTimeout as delay } from "node:timers/promises";
15
+ import { fileURLToPath } from "node:url";
16
+ import { quote } from "shell-quote";
17
+ import { retainedBrowserEnvironment, validateCheckEnvironment } from "./check-environment.js";
18
+ import { ensureRunControllerStorage, getDatabase } from "../storage/database.js";
19
+ import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
20
+ import { publishImmutableInput } from "../storage/immutable-input.js";
21
+ import { AppError } from "../shared/errors.js";
22
+ import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
23
+ import { parseRuntimeBudget } from "./runtime-budget.js";
24
+ import { requireProjectByRoot } from "./projects.js";
25
+ import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
26
+ import { getFlowRunStatus, setFlowRunRecoveryPaused, assertPreparedRunBundleSqlAuthority } from "./runs.js";
27
+ import { loadExternalStageContext } from "./stage-context.js";
28
+ import { flowCommand, stageResumeSourcePath } from "./stage-pause.js";
29
+ import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
30
+ import { selectControllerExecution } from "./controller-execution.js";
31
+ import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
32
+ import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
33
+ import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess, processTreeIsAlive, resourceHome } from "./managed-processes.js";
34
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
35
+ import { physicalStartup } from "./physical-startup.js";
36
+ import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecutionAbsent, managedExecutionReusable } from "./managed-daemon-binding.js";
37
+ import { initCodexHome } from "./hooks.js";
38
+ import { launchExternalWork } from "./external-work-launch.js";
39
+ import { runControllerMemoryTask } from "./controller-memory-task.js";
40
+ import { controllerNativeChildren, controllerSemanticCheckpoint, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
41
+ import { publishPromptDelivery } from "./prompt-delivery.js";
42
+ import { publishStagePacket } from "./stage-report-renderer.js";
43
+ import { claimQueuedMergeRequest } from "./merge-server.js";
44
+ import { prepareVnextCodeWorkspace } from "./vnext-code.js";
45
+ import { requireRun, requireController, controllerReceipt, controllerDependencyDiagnostic, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
46
+ import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
47
+ import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
48
+ import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
49
+ import { retainedChildObserver } from "./controller-child-observer.js";
50
+ import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure, ownedProviderBoundary, unavailableProviderChildRecovery } from "./provider-capacity.js";
51
+ import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
52
+ import { captureControllerBoundary } from "./run-controller-capture.js";
53
+ import { requestRunControl } from "./run-control-worker.js";
54
+ import { recordRunControlIntent } from "./run-control.js";
55
+ import { storageSessionId } from "./session-identity.js";
56
+ import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhausted as overloadExhausted, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, normalizeRecoveryFailure } from "../harness-runtime/lib/provider-overload-policy.mjs";
57
+ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
58
+ import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
59
+ import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
60
+ import { prepareWorkFailureResolution, readFailureDecision, failureDecisionReady, acceptFailureDecision, applyWorkFailureDecision, reconcilePendingWorkFailure, workFailureResolutionPrompt, workFailureDiagnostic, workFailureBlockActive } from "./controller-work-failure.js";
61
+ import { getVnextFanoutStatus } from "./vnext-fanout.js";
62
+ import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
63
+ import { recordTechnicalNote } from "./technical-notes.js";
64
+ import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
65
+ export { resumeRunController } from "./run-controller-recovery.js";
66
+ const preparedLaunchInputs = new WeakMap();
67
+ function launchInputDigest(input) {
68
+ return digest([input.projectRoot, input.runId, input.requestId, input.contextFile, input.contextSha256,
69
+ input.stopAfter ?? null, input.captureRoot ?? null]);
70
+ }
71
+ export function prepareRunControllerLaunch(context, input) {
72
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
73
+ throw new AppError("usage", "Managed launch needs a request id and context SHA-256", 2);
74
+ if (input.stopAfter !== undefined && !/^[a-z][a-z-]*$/.test(input.stopAfter))
75
+ throw new AppError("usage", "Invalid stop-after stage", 2);
76
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
77
+ const run = requireRun(context, project.id, input.runId);
78
+ const captureRoot = input.captureRoot === undefined ? undefined : canonicalPath(input.captureRoot, false);
79
+ if (captureRoot && [context.ddFlowHome, project.root, run.workspace_root].some(root => { const relative = path.relative(canonicalPath(root, false), captureRoot); return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); }))
80
+ throw new AppError("snapshot_output_inside_runtime", "Controller captures require a root outside the runtime, project and workspace", 2);
81
+ if (captureRoot && fs.existsSync(captureRoot) && !fs.statSync(captureRoot).isDirectory())
82
+ throw new AppError("usage", "Controller capture root must be a directory", 2);
83
+ const budget = parseRuntimeBudget(context.env.DD_FLOW_RUNTIME_BUDGET);
84
+ const request = { project_root: project.root, run_id: run.id, context_sha256: input.contextSha256, stop_after: input.stopAfter ?? null, ...(captureRoot ? { capture_root: captureRoot } : {}), ...(budget ? { budget_sha256: digest(budget) } : {}) };
85
+ const requestHash = digest(request);
86
+ const existing = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")
87
+ ? context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]) : undefined;
88
+ if (existing) {
89
+ if (existing.request_hash !== requestHash)
90
+ throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
91
+ selectControllerExecution(context, run, JSON.parse(existing.manifest_json));
92
+ const prepared = { kind: "replay", existing };
93
+ preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
94
+ return prepared;
95
+ }
96
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
97
+ if (run.status !== "running")
98
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1, { status: run.status });
99
+ const routing = selectControllerExecution(context, run);
100
+ const contexts = readContexts(input.contextFile, input.contextSha256);
101
+ if (routing.bundle) {
102
+ for (const stage of [...Object.keys(contexts), ...(input.stopAfter ? [input.stopAfter] : [])])
103
+ routing.bundle.descriptor.stage(stage);
104
+ }
105
+ const next = getFlowRunStatus(context, { projectRoot: project.root, runId: run.id }).continuation;
106
+ if (next.kind !== "start_stage" || !next.stage)
107
+ throw new AppError("controller_attach_required", "Managed launch cannot replace an attached or unfinished coordinator; reconcile its existing owner", 1, { continuation: next });
108
+ if (!contexts[next.stage])
109
+ throw new AppError("controller_context_missing", "Managed context omits the first stage", 2, { stage: next.stage });
110
+ const adapters = {};
111
+ for (const profile of Object.values(routing.profiles)) {
112
+ const harness = runtimeHarness(profile);
113
+ adapters[harness] = { executable: resolveHarnessCommand(context.ddFlowHome, harness, "adapter"), runtime_args: harnessRuntimeArguments(context.ddFlowHome, harness) };
114
+ }
115
+ const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
116
+ if (!fs.existsSync(entry))
117
+ throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
118
+ const prepared = { kind: "create", project, run, request, requestHash, generation, budget, routing, contexts, adapters };
119
+ preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
120
+ return prepared;
121
+ }
122
+ /** Retain the existing launch intent and frozen inputs without starting a
123
+ * process. Dispatch remains exclusively owned by launchRunController. */
124
+ export async function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
125
+ const selection = preparedLaunchInputs.get(prepared);
126
+ if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
127
+ throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
128
+ }
129
+ if (prepared.kind === "replay") {
130
+ const current = requireController(context, prepared.existing.controller_id);
131
+ if (current.request_hash !== prepared.existing.request_hash)
132
+ throw new AppError("runtime_request_conflict", "Controller launch authority changed after replay preparation", 1);
133
+ selectControllerExecution(context, requireRun(context, current.project_id, current.run_id), JSON.parse(current.manifest_json));
134
+ return { controller: current, reused: true };
135
+ }
136
+ const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
137
+ const assertCurrent = () => {
138
+ if (selection.sha256 !== launchInputDigest(input))
139
+ throw new AppError("runtime_request_conflict", "Controller launch changed during physical observation", 1, { effect: "no_effect" });
140
+ assertRunMutationAllowed(context, project.id, run.id, generation);
141
+ if (requireRun(context, project.id, run.id).status !== "running")
142
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
143
+ };
144
+ const launcher = await physicalStartup(context.databaseDeadlineAt).birth(assertCurrent);
145
+ ensureRunControllerStorage(context.db);
146
+ const controllerId = `DRV-${crypto.randomUUID()}`;
147
+ const root = path.join(run.run_root, "controllers", controllerId);
148
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
149
+ const flowExecutable = prepareHarnessFlowExecutable(context, root, routing.bundle?.engine());
150
+ const frozenContexts = {};
151
+ for (const [stage, value] of Object.entries(contexts)) {
152
+ const file = path.join(root, `${stage}.context.json`);
153
+ fs.writeFileSync(file, value.bytes, { flag: "wx", mode: 0o600 });
154
+ frozenContexts[stage] = { file, sha256: value.sha256 };
155
+ }
156
+ const manifest = { schema_id: routing.kind === "descriptor" ? "dd-flow/run-controller-manifest@2" : "dd-flow/run-controller-manifest@1", request,
157
+ ...(routing.kind === "descriptor" ? { execution_binding: routing.binding } : { execution_profile: routing.legacy }),
158
+ contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget,
159
+ ...(retainedBrowserEnvironment(context.env) ? { check_environment: retainedBrowserEnvironment(context.env) } : {}) };
160
+ const token = crypto.randomUUID();
161
+ const now = context.now();
162
+ const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
163
+ const duplicate = context.db.writeTransaction(() => {
164
+ assertCurrent();
165
+ if (routing.bundle)
166
+ assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
167
+ const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
168
+ if (duplicate) {
169
+ if (duplicate.request_hash !== requestHash)
170
+ throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
171
+ return duplicate;
172
+ }
173
+ assertRunMutationAllowed(context, project.id, run.id, generation);
174
+ if (requireRun(context, project.id, run.id).status !== "running")
175
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
176
+ const owner = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [project.id, run.id]);
177
+ if (owner)
178
+ throw new AppError("controller_already_owned", "RUN already has an unresolved managed owner; observe or reconcile it", 1, { controller_id: owner.controller_id });
179
+ if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.status = 'running' LIMIT 1", [project.id, run.id]))
180
+ throw new AppError("controller_attach_required", "A WorkSession acquired the RUN before managed launch", 1);
181
+ context.db.run("INSERT INTO run_controllers (controller_id, project_id, run_id, request_id, request_hash, generation, status, owner_token, manifest_json, state_json, state_dir, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?)", [controllerId, project.id, run.id, input.requestId, requestHash, generation, token, JSON.stringify(manifest), JSON.stringify(state), root, now, now]);
182
+ recordControllerLauncher(context, requireController(context, controllerId), undefined, launcher);
183
+ appendEvent(context, controllerId, "launch_requested", { request_id: input.requestId, generation, request_hash: requestHash });
184
+ return null;
185
+ });
186
+ return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
187
+ }
188
+ export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
189
+ const retained = await retainRunControllerLaunch(context, input, prepared);
190
+ // A simultaneous publisher already owns dispatch. Preserve the original
191
+ // race behavior: only an explicit replay may reconcile that existing owner.
192
+ if (prepared.kind === "create" && retained.reused)
193
+ return { ...controllerReceipt(retained.controller), reused: true };
194
+ const receipt = await spawnController(context, retained.controller);
195
+ return retained.reused ? { ...receipt, reused: true } : receipt;
196
+ }
197
+ /**
198
+ * A registry write can be temporarily unavailable while another runtime client
199
+ * owns SQLite's writer lock. That is not evidence that this controller lost
200
+ * its lease. Productive dispatch still requires a successful recheck before
201
+ * proceeding, while an explicit negative renewal remains a hard ownership
202
+ * loss.
203
+ */
204
+ export const controllerLeaseMonitor = managedLeaseMonitor;
205
+ /** Unconfirmed ownership is not evidence authorizing a native cancellation. */
206
+ export function retainControllerLeaseUncertainty(context, row, error) {
207
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
208
+ return false;
209
+ try {
210
+ const db = context.db.withDeadline(Date.now() + 5_000);
211
+ db.writeTransaction(() => {
212
+ const changed = db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND generation = ? AND status NOT IN ('completed','cancelled','superseded','recovery_required','capture_failed')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token, row.generation]);
213
+ if (changed.changes)
214
+ appendEvent({ ...context, db }, row.controller_id, "owner_confirmation_required", { ...errorRecord(error), native_cancelled: false });
215
+ });
216
+ }
217
+ catch (persistence) {
218
+ process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, primary: errorRecord(error), persistence: errorRecord(persistence), native_cancelled: false })}\n`);
219
+ }
220
+ return true;
221
+ }
222
+ /** No initialization, provider calls or lease takeover on the observation path. */
223
+ export function runControllerStatus(context, input) {
224
+ if (input.after !== undefined && (!Number.isSafeInteger(input.after) || input.after < 0))
225
+ throw new AppError("usage", "--after must be a non-negative cursor", 2);
226
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
227
+ const run = requireRun(context, project.id, input.runId);
228
+ if (!context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'"))
229
+ return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
230
+ const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, controller_id DESC LIMIT 1", [project.id, run.id]);
231
+ if (!row)
232
+ return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
233
+ const events = context.db.all("SELECT sequence, event_type, data_json, created_at FROM run_controller_events WHERE controller_id = ? AND sequence > ? ORDER BY sequence LIMIT 100", [row.controller_id, input.after ?? 0]);
234
+ return { ...controllerReceipt(row, context), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
235
+ }
236
+ export async function serveRunController(context, input) {
237
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
238
+ const row = requireController(context, input.controllerId);
239
+ if (row.project_id !== project.id || row.run_id !== input.runId || row.owner_token !== context.env.DD_FLOW_CONTROLLER_TOKEN)
240
+ throw new AppError("controller_owner_mismatch", "Managed serve does not own this launch", 1);
241
+ let lease;
242
+ let mergeLease;
243
+ let heartbeat;
244
+ let state;
245
+ let claimedProcess = false;
246
+ let primaryFailure;
247
+ const retainSecondary = (phase, error) => {
248
+ process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, phase, primary: errorRecord(primaryFailure), secondary: errorRecord(error) })}\n`);
249
+ };
250
+ try {
251
+ const currentState = state = JSON.parse(row.state_json);
252
+ const startup = physicalStartup(context.databaseDeadlineAt);
253
+ const assertStartingOwner = () => {
254
+ const current = requireController(context, row.controller_id);
255
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.owner_pid !== null || current.status !== "starting")
256
+ throw new AppError("controller_owner_mismatch", "Controller startup no longer owns this launch", 1);
257
+ if (["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
258
+ throw new AppError("invalid_run_state", "A terminal RUN cannot start a physical controller", 1);
259
+ };
260
+ const owner = await startup.birth(assertStartingOwner);
261
+ try {
262
+ claimControllerProcess(context, row, owner);
263
+ }
264
+ finally {
265
+ claimedProcess = row.owner_pid === process.pid;
266
+ }
267
+ if (!row.process_id || !row.process_lease_token)
268
+ throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
269
+ const confirmation = { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid };
270
+ const originalLease = prepareManagedProcessConfirmation(context, confirmation).lease_expires_at;
271
+ const confirmed = await startup.observe(() => confirmManagedProcess(context, confirmation), () => assertControllerLeaseAllowed(context, row), originalLease);
272
+ lease = controllerLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: row.process_id, leaseToken: row.process_lease_token }), confirmed.lease_expires_at);
273
+ // Resource and RUN commits are distinct effects. A RUN keepalive failure must
274
+ // neither erase a resource receipt nor grant RUN dispatch authority.
275
+ mergeLease = controllerLeaseMonitor(attemptMs => {
276
+ const expiry = new Date(Date.now() + 120_000).toISOString();
277
+ const db = context.db.withDeadline(Date.now() + attemptMs);
278
+ if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
279
+ db.writeTransaction(() => {
280
+ const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
281
+ if ((guard?.generation ?? 0) !== row.generation || ["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
282
+ throw new AppError("controller_lease_lost", "RUN generation changed before MERGE keepalive", 1);
283
+ db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [expiry, context.now(), row.project_id, row.run_id, row.controller_id]);
284
+ });
285
+ return { owned: true, lease_expires_at: expiry };
286
+ });
287
+ const assertLeaseFence = () => {
288
+ assertControllerLeaseAllowed(context, row);
289
+ };
290
+ heartbeat = setInterval(() => {
291
+ lease.background(assertLeaseFence, () => { });
292
+ if (currentState.merge_claim)
293
+ mergeLease.background(assertLeaseFence, () => { });
294
+ }, 30_000);
295
+ heartbeat.unref();
296
+ const manifest = JSON.parse(row.manifest_json);
297
+ const assertPhysicalOwner = () => {
298
+ lease.assertNoRenewal();
299
+ if (currentState.merge_claim)
300
+ mergeLease.assertNoRenewal();
301
+ // This isolated read already bounds SQLite contention. A wall deadline
302
+ // before opening the reader mistakes scheduler delay for heartbeat loss;
303
+ // the observed row's actual expiry/token still fences productive dispatch.
304
+ assertManagedProcessLease(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
305
+ const current = requireController(context, row.controller_id);
306
+ if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
307
+ throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
308
+ };
309
+ const assertOwner = () => { assertPhysicalOwner(); assertRunMutationAllowed(context, row.project_id, row.run_id, row.generation); };
310
+ await lease.ensureConfirmed(assertLeaseFence);
311
+ if (currentState.merge_claim)
312
+ await mergeLease.ensureConfirmed(assertLeaseFence);
313
+ context.db.writeTransaction(() => {
314
+ markControllerDispatchReady(context, row);
315
+ if (!currentState.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
316
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, false);
317
+ });
318
+ appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
319
+ if (currentState.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
320
+ await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
321
+ await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
322
+ await mergeLease.ensureConfirmed(assertLeaseFence); });
323
+ return controllerReceipt(requireController(context, row.controller_id), context);
324
+ }
325
+ catch (error) {
326
+ primaryFailure = error;
327
+ try {
328
+ const controlled = context.db.writeTransaction(() => {
329
+ const currentOwner = requireController(context, row.controller_id);
330
+ const sameBinding = currentOwner.owner_token === row.owner_token && currentOwner.generation === row.generation
331
+ && currentOwner.process_id === row.process_id && currentOwner.process_lease_token === row.process_lease_token;
332
+ const ownsFailure = claimedProcess ? currentOwner.owner_pid === process.pid : row.owner_pid === null && currentOwner.owner_pid === null && row.status === "starting" && currentOwner.status === "starting";
333
+ if (!sameBinding || !ownsFailure || ["completed", "cancelled", "superseded"].includes(currentOwner.status))
334
+ throw error;
335
+ try {
336
+ if (state)
337
+ failControllerRecovery(context, row, state, error);
338
+ }
339
+ catch (secondary) {
340
+ retainSecondary("fail_controller_recovery", secondary);
341
+ }
342
+ if ((error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code) || (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) !== row.generation)
343
+ && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
344
+ const guard = recoveryGuard(context, row.project_id, row.run_id);
345
+ const control = guard && ["draining", "sealed"].includes(guard.status) && context.db.get("SELECT control_id, action FROM run_controls WHERE project_id = ? AND run_id = ? AND recovery_id = ? AND status IN ('requested','settled')", [row.project_id, row.run_id, guard.recovery_id]);
346
+ if (control) {
347
+ // Control is a later state transition, not evidence that the causal
348
+ // failure which fenced admission disappeared.
349
+ context.db.run("UPDATE run_controllers SET status = CASE WHEN last_error_json IS NULL THEN 'control_requested' ELSE status END, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [context.now(), row.controller_id, row.owner_token]);
350
+ appendEvent(context, row.controller_id, "control_boundary_reached", { control_id: control.control_id, action: control.action, generation: guard.generation, settled: false });
351
+ return controllerReceipt(requireController(context, row.controller_id));
352
+ }
353
+ }
354
+ try {
355
+ // A failed capture must not reopen admission when this process exits.
356
+ const changed = context.db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status NOT IN ('completed','cancelled','superseded')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token]);
357
+ context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status IN ('dispatching','active')", [JSON.stringify(errorRecord(error)), context.now(), row.project_id, row.run_id, row.controller_id]);
358
+ if (changed.changes && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation) {
359
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
360
+ }
361
+ appendEvent(context, row.controller_id, "recovery_required", errorRecord(error));
362
+ }
363
+ catch (failure) {
364
+ retainSecondary("persist_controller_failure", failure);
365
+ }
366
+ return null;
367
+ });
368
+ if (controlled)
369
+ return controlled;
370
+ }
371
+ catch (secondary) {
372
+ retainSecondary("controller_failure_boundary", secondary);
373
+ }
374
+ throw error;
375
+ }
376
+ finally {
377
+ lease?.close();
378
+ mergeLease?.close();
379
+ if (heartbeat)
380
+ clearInterval(heartbeat);
381
+ finishOwner();
382
+ }
383
+ function finishOwner() {
384
+ if (!claimedProcess || row.owner_pid !== process.pid || !row.process_id || !row.process_lease_token)
385
+ return;
386
+ try {
387
+ finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
388
+ }
389
+ catch (cleanup) {
390
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "controller_resource_retirement_pending", message: "Controller execution ended; resource-registry retirement remains pending.", phase: "controller_cleanup", generation: row.generation, resource_identity: row.process_id, consequence: "cleanup_pending" });
391
+ retainSecondary("finish_controller_process", cleanup);
392
+ }
393
+ }
394
+ }
395
+ export function prepareRunControllerAnswer(context, input) {
396
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !input.pauseId.trim())
397
+ throw new AppError("usage", "Answer requires request-id and pause-id", 2);
398
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
399
+ const prior = context.db.get(`SELECT o.operation_id, o.status, o.input_json
400
+ FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id
401
+ WHERE c.project_id = ? AND c.run_id = ? AND o.operation_id = c.controller_id || ':answer:' || ?`, [project.id, input.runId, input.requestId]);
402
+ if (prior) {
403
+ const retained = JSON.parse(prior.input_json);
404
+ if (retained.pause_id !== input.pauseId)
405
+ throw new AppError("runtime_request_conflict", "HITL answer request id belongs to another pause", 1, { operation_id: prior.operation_id });
406
+ return { kind: "replay", operationId: prior.operation_id, status: prior.status };
407
+ }
408
+ const { bytes } = prepareFileBytes(input.answerFile, "HITL answer");
409
+ if (!bytes.toString("utf8").trim())
410
+ throw new AppError("validation", "HITL answer must not be empty", 2);
411
+ const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [project.id, input.runId]);
412
+ if (!row)
413
+ throw new AppError("not_found", "RUN has no managed controller for this answer", 1);
414
+ const operationId = `${row.controller_id}:answer:${input.requestId}`;
415
+ const answerHash = digestBytes(bytes);
416
+ const hash = digest({ pause_id: input.pauseId, answer_sha256: answerHash });
417
+ const previous = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
418
+ if (previous.prior)
419
+ return { kind: "replay", operationId, status: previous.prior.status };
420
+ return { kind: "create", project, row, bytes, operationId, answerHash, hash };
421
+ }
422
+ function prepareControllerAnswerAcceptance(context, input, projectId, row, operationId, hash) {
423
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
424
+ if (prior) {
425
+ if (prior.input_hash !== hash)
426
+ throw new AppError("runtime_request_conflict", "HITL answer request id has different raw input", 1);
427
+ return { prior };
428
+ }
429
+ const currentOwner = requireController(context, row.controller_id);
430
+ if (["completed", "cancelled", "superseded"].includes(currentOwner.status))
431
+ throw new AppError("invalid_run_state", "The answer's controller is no longer current", 1);
432
+ const competing = context.db.get("SELECT operation_id FROM run_controller_operations WHERE controller_id = ? AND json_extract(input_json, '$.action') = 'user_answer' AND json_extract(input_json, '$.pause_id') = ? LIMIT 1", [row.controller_id, input.pauseId]);
433
+ if (competing)
434
+ throw new AppError("runtime_request_conflict", "This HITL pause already has an accepted answer", 1, { operation_id: competing.operation_id });
435
+ const run = requireRun(context, projectId, input.runId);
436
+ if (["done", "failed", "cancelled"].includes(run.status))
437
+ throw new AppError("invalid_run_state", "A terminal RUN cannot accept a new answer", 1);
438
+ const index = JSON.parse(run.index_json);
439
+ const stage = index.stage_runs.find(stage => stage.pause?.id === input.pauseId && ["paused", "waiting_for_user"].includes(stage.status));
440
+ if (!stage?.pause?.work_id)
441
+ throw new AppError("hitl_pause_mismatch", "Answer does not match the RUN's current HITL pause", 1);
442
+ return { run, stage, workId: stage.pause.work_id };
443
+ }
444
+ export function answerRunController(context, input, prepared = prepareRunControllerAnswer(context, input)) {
445
+ if (prepared.kind === "replay")
446
+ return { ok: true, operation_id: prepared.operationId, status: prepared.status, reused: true };
447
+ const { project, row, bytes, operationId, answerHash, hash } = prepared;
448
+ const previous = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
449
+ if (previous.prior)
450
+ return { ok: true, operation_id: operationId, status: previous.prior.status, reused: true };
451
+ const file = path.join(row.state_dir, `answer-${digest(operationId)}-${answerHash}.md`);
452
+ publishImmutableInput(file, bytes, "runtime_request_conflict");
453
+ return context.db.writeTransaction(() => {
454
+ // Recheck only mutable acceptance state under the write transaction; bytes are already prepared.
455
+ const acceptance = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
456
+ if (acceptance.prior) {
457
+ return { ok: true, operation_id: operationId, status: acceptance.prior.status, reused: true };
458
+ }
459
+ const { run, stage, workId } = acceptance;
460
+ if (requireController(context, row.controller_id).state_dir !== row.state_dir)
461
+ throw new AppError("controller_owner_mismatch", "Controller input directory changed before acceptance", 1);
462
+ const now = context.now();
463
+ const generation = recoveryGuard(context, project.id, run.id)?.generation ?? row.generation;
464
+ context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?)", [operationId, row.controller_id, generation, hash, JSON.stringify({ action: "user_answer", pause_id: input.pauseId, stage: stage.stage, work_id: workId, file, sha256: answerHash }), now, now]);
465
+ appendEvent(context, row.controller_id, "answer_accepted", { operation_id: operationId, pause_id: input.pauseId, answer_sha256: answerHash });
466
+ return { ok: true, operation_id: operationId, status: "accepted" };
467
+ });
468
+ }
469
+ export function prepareRunControllerContext(context, input) {
470
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-z][a-z-]*$/.test(input.stage) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
471
+ throw new AppError("usage", "Context submission requires request id, stage and SHA-256", 2);
472
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
473
+ const operationId = `${input.controllerId}:context:${input.requestId}`;
474
+ const hash = digest({ stage: input.stage, sha256: input.contextSha256 });
475
+ const row = requireController(context, input.controllerId);
476
+ if (row.project_id !== project.id || row.run_id !== input.runId)
477
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
478
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
479
+ if (prior) {
480
+ if (prior.input_hash !== hash)
481
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
482
+ return { kind: "replay", operationId, status: prior.status };
483
+ }
484
+ availableControllerContext(context, input, row);
485
+ const { bytes } = loadExternalStageContext({ file: input.contextFile, expectedSha256: input.contextSha256, stage: input.stage });
486
+ return { kind: "create", project, operationId, hash, bytes };
487
+ }
488
+ export function supplyRunControllerContext(context, input, prepared = prepareRunControllerContext(context, input)) {
489
+ if (prepared.kind === "replay")
490
+ return { ok: true, operation_id: prepared.operationId, status: prepared.status, reused: true };
491
+ const { project, operationId, hash, bytes } = prepared;
492
+ const preparedOwner = requireController(context, input.controllerId);
493
+ if (preparedOwner.project_id !== project.id || preparedOwner.run_id !== input.runId)
494
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
495
+ const existing = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
496
+ if (existing) {
497
+ if (existing.input_hash !== hash)
498
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
499
+ return { ok: true, operation_id: operationId, status: existing.status, reused: true };
500
+ }
501
+ availableControllerContext(context, input, preparedOwner);
502
+ const file = path.join(preparedOwner.state_dir, `${input.stage}-${input.contextSha256}.context.json`);
503
+ publishImmutableInput(file, bytes, "stage_context_replacement");
504
+ return context.db.writeTransaction(() => {
505
+ const row = requireController(context, input.controllerId);
506
+ if (row.project_id !== project.id || row.run_id !== input.runId)
507
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
508
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
509
+ if (prior) {
510
+ if (prior.input_hash !== hash)
511
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
512
+ return { ok: true, operation_id: operationId, status: prior.status, reused: true };
513
+ }
514
+ const manifest = availableControllerContext(context, input, row);
515
+ if (row.state_dir !== preparedOwner.state_dir)
516
+ throw new AppError("controller_owner_mismatch", "Controller input directory changed before acceptance", 1);
517
+ manifest.contexts[input.stage] = { file, sha256: input.contextSha256 };
518
+ const now = context.now();
519
+ context.db.run("UPDATE run_controllers SET manifest_json = ?, updated_at = ? WHERE controller_id = ?", [JSON.stringify(manifest), now, row.controller_id]);
520
+ context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?)", [operationId, row.controller_id, row.generation, hash, JSON.stringify({ action: "stage_context", stage: input.stage, file, sha256: input.contextSha256 }), now, now]);
521
+ appendEvent(context, row.controller_id, "context_accepted", { operation_id: operationId, stage: input.stage, context_sha256: input.contextSha256 });
522
+ return { ok: true, operation_id: operationId, status: "accepted" };
523
+ });
524
+ }
525
+ function availableControllerContext(context, input, row) {
526
+ assertRunMutationAllowed(context, row.project_id, input.runId, row.generation);
527
+ if (!["starting", "running", "waiting_for_context", "waiting_for_user", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(row.status))
528
+ throw new AppError("invalid_run_state", "Controller cannot accept a new context in its current state", 1);
529
+ const manifest = JSON.parse(row.manifest_json);
530
+ if (manifest.contexts[input.stage])
531
+ throw new AppError("stage_context_replacement", "Controller already has an immutable context for this stage", 1);
532
+ return manifest;
533
+ }
534
+ async function executeController(context, row, manifest, state, assertOwnership, assertPhysicalOwner, renewLease) {
535
+ assertOwnership();
536
+ const observer = { ...context, db: getDatabase(context.ddFlowHome, "read_existing") };
537
+ const checkOutcomes = () => assertLifecycleOutcomes(observer, { projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
538
+ const assertOwner = () => { if (fatal)
539
+ throw fatal; checkOutcomes(); assertOwnership(); };
540
+ const prepareMutation = async (action) => {
541
+ for (;;) {
542
+ await renewLease();
543
+ try {
544
+ context.db.writeTransaction(() => { assertOwner(); action(); });
545
+ return;
546
+ }
547
+ catch (error) {
548
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
549
+ throw error;
550
+ }
551
+ }
552
+ };
553
+ const routing = selectControllerExecution(context, requireRun(context, row.project_id, row.run_id), manifest);
554
+ const policy = routing.policy;
555
+ const profiles = routing.profiles;
556
+ let executionContext = { ...context, env: { ...context.env, ...validateCheckEnvironment(manifest.check_environment), DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
557
+ const bindInvocationScope = (session) => {
558
+ executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
559
+ projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
560
+ };
561
+ let lastCapture = null;
562
+ let terminalFinalization = false;
563
+ let technicalPauseRequested = false;
564
+ let fatal;
565
+ let stopping;
566
+ const cancellation = new AbortController();
567
+ const observationCancellation = new AbortController();
568
+ const childObservers = new Map();
569
+ let promptPending = false;
570
+ let observedPromptOperation = null;
571
+ const childObserver = (session) => {
572
+ if (runtimeHarness(session.profile) !== "zcode-acp" || !state.stage || !["plan-review", "code", "code-review"].includes(state.stage))
573
+ return undefined;
574
+ if (readNativeDaemonState(session.state_dir).capabilities?.retained_child_observation !== true)
575
+ return undefined;
576
+ const scope = { controller_id: row.controller_id, generation: row.generation, stage: state.stage, root_session_id: session.id };
577
+ const key = JSON.stringify(scope);
578
+ let retained = childObservers.get(key);
579
+ if (!retained) {
580
+ retained = retainedChildObserver({ file: path.join(row.state_dir, "native-child-observations.jsonl"), scope, assertOwner: assertOwnership,
581
+ gap: error => { process.stderr.write(`${JSON.stringify({ event: "native_child_observation_gap", scope, error_code: String(error?.code ?? "observation_unavailable") })}\n`); },
582
+ read: async () => {
583
+ assertOwnership();
584
+ const operation = observedPromptOperation;
585
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
586
+ let receipt;
587
+ try {
588
+ receipt = await runHarnessAdapter({ executable: manifest.adapters["zcode-acp"].executable,
589
+ args: ["session", "observe-children", "--session-id", session.id, ...sessionArguments(context, manifest, session)],
590
+ env: controllerEnvironment(context, manifest, session), operationId: crypto.randomUUID(), harness: "zcode", timeoutMs: 30_000,
591
+ signal: AbortSignal.any([cancellation.signal, observationCancellation.signal]) });
592
+ }
593
+ catch (error) {
594
+ if (!promptPending || observedPromptOperation !== operation)
595
+ return null;
596
+ throw error;
597
+ }
598
+ if (!promptPending || observedPromptOperation !== operation || receipt.active_operation_id === null)
599
+ return null;
600
+ if (receipt.schema_id !== "dd-zcode/child-observation@1" || receipt.provider_session_id !== session.id || receipt.daemon_id !== binding.state.daemon_id)
601
+ throw new AppError("native_child_observation_conflict", "Retained child observation has foreign daemon/root identity", 1);
602
+ if (receipt.active_operation_id !== operation)
603
+ throw new AppError("native_child_observation_conflict", "Retained observation has a foreign productive operation", 1);
604
+ const children = controllerNativeChildren(receipt, session.id).filter(child => !(state.native_baseline ?? []).includes(child.session_id));
605
+ if (children.some(child => child.status === "failed" && !child.terminal_turn_id))
606
+ throw new AppError("operation_observation_lost", "Retained child terminal identity is unavailable", 1);
607
+ return children;
608
+ } });
609
+ childObservers.set(key, retained);
610
+ }
611
+ return retained;
612
+ };
613
+ const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
614
+ const stopAfterFailure = (error) => {
615
+ // The published pause already fences this owner. A later observer failure
616
+ // must not supersede that same control with a second pause or fatal stop.
617
+ if (technicalPauseRequested)
618
+ return Promise.resolve();
619
+ // A retained successor may already be running. Fencing/recovery records
620
+ // uncertainty; cancellation is not evidence of its missing outcome.
621
+ if (capacityUncertain(error))
622
+ return Promise.resolve();
623
+ if (retainControllerLeaseUncertainty(context, row, error))
624
+ return Promise.resolve();
625
+ if (error instanceof AppError && error.code !== "child_work_repair_blocked" && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
626
+ return pauseDependency(state.sessions[state.current_session], undefined, error, "productive_continuation").catch(cause => {
627
+ if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
628
+ return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
629
+ });
630
+ if (stopping)
631
+ return stopping;
632
+ fatal = error;
633
+ // Persist the cause before fencing/cancellation; later control is not an
634
+ // operator pause and cannot replace this execution failure.
635
+ stopping = (async () => {
636
+ try {
637
+ context.db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token]);
638
+ }
639
+ catch (persistence) {
640
+ process.stderr.write(`${JSON.stringify({ primary: errorRecord(error), persistence: errorRecord(persistence) })}\n`);
641
+ }
642
+ await requestRunControl(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id, requestId: `fatal:${row.controller_id}:${row.generation}`, action: "stop", waitMs: 0 });
643
+ })().catch(cleanup => {
644
+ // The caller still receives the primary failure if control cannot start.
645
+ try {
646
+ appendEvent(context, row.controller_id, "failure_control_error", { primary: errorRecord(error), cleanup: errorRecord(cleanup) });
647
+ }
648
+ catch {
649
+ process.stderr.write(`${JSON.stringify({ primary: errorRecord(error), cleanup: errorRecord(cleanup) })}\n`);
650
+ }
651
+ }).finally(() => cancellation.abort());
652
+ return stopping;
653
+ };
654
+ const setStatus = (status, terminalData) => {
655
+ context.db.writeTransaction(() => {
656
+ const changed = context.db.run("UPDATE run_controllers SET status = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status <> ?", [status, context.now(), row.controller_id, row.owner_token, status]);
657
+ if (changed.changes) {
658
+ appendEvent(context, row.controller_id, "status_changed", { status, stage: state.stage });
659
+ if (terminalData)
660
+ appendEvent(context, row.controller_id, status, terminalData);
661
+ }
662
+ });
663
+ };
664
+ const call = async (session, args, productive = true, operationId) => {
665
+ const receipt = await controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, operationId, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
666
+ const previous = state.last_receipt;
667
+ // A read refreshes native observations, not the already accepted engine
668
+ // boundary. Never copy native readiness/settlement fields from the old read.
669
+ return args[0] === "session" && args[1] === "inspect" && previous?.accepted_boundary && adapterSessionId(previous) === session.id
670
+ ? { ...receipt, accepted_boundary: previous.accepted_boundary } : receipt;
671
+ };
672
+ const note = (session, code, phase, consequence, operationId, noteState) => recordTechnicalNote(context, row.project_id, row.run_id, {
673
+ code, message: "The accepted result is retained independently of native session retirement or observation.", phase,
674
+ generation: row.generation, ...(state.stage ? { stage: state.stage } : {}), harness: runtimeHarness(session.profile),
675
+ resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence, ...(operationId ? { source_operation_id: operationId } : {}), ...(noteState ? { state: noteState } : {})
676
+ });
677
+ const failedChildren = (session) => (childObserver(session)?.failed() ?? []).filter(child => {
678
+ if (!acceptedChildWork(context, row, runtimeHarness(session.profile), session.id, child.session_id))
679
+ return true;
680
+ note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
681
+ return false;
682
+ });
683
+ const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
684
+ // The control intent contains the retained productive failure; it must not
685
+ // require that same failure to disappear before the owner can pause it.
686
+ assertPhysicalOwner();
687
+ const recorded = cause ? errorRecord(cause) : { code: "native_writer_dependency_blocked", message: "Conflicting native execution has not settled" };
688
+ const dependency = controllerDependencyDiagnostic(recorded, protectedAction);
689
+ const primary = dependency.primary;
690
+ const details = cause instanceof AppError ? cause.details : {};
691
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: primary.code, message: primary.message,
692
+ phase: protectedAction, generation: row.generation, ...(state.stage ? { stage: state.stage } : {}),
693
+ ...(typeof details.work_id === "string" ? { work_id: details.work_id } : {}),
694
+ harness: runtimeHarness(session.profile), resource_identity: session.state_dir,
695
+ consequence: "dependency_blocked", ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}),
696
+ diagnostic_reference: JSON.stringify(dependency) });
697
+ // Diagnostic observations can race (adapter read vs lifecycle monitor), but
698
+ // they own one pause of this execution, not one control per error message.
699
+ const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
700
+ const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
701
+ // Publish source binding in the same SQL transaction as the control
702
+ // intent. A detached worker must never observe an unlabelled pause.
703
+ const intent = recordRunControlIntent(executionContext, input, receipt => {
704
+ const current = requireController(context, row.controller_id);
705
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.process_id !== row.process_id)
706
+ throw new AppError("controller_owner_mismatch", "Controller changed before its technical control was recorded", 1);
707
+ const controlId = receipt.control_id;
708
+ if (typeof controlId !== "string")
709
+ return;
710
+ const prior = context.db.get("SELECT 1 FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=? AND COALESCE(json_extract(data_json,'$.source_operation_id'),'')=?", [row.controller_id, controlId, sourceOperationId ?? ""]);
711
+ if (!prior)
712
+ appendEvent(context, row.controller_id, "technical_dependency_paused", { schema_id: "dd-flow/controller-dependency@1", request_id: requestId, control_id: controlId, generation: row.generation, control: receipt, state_dir: session.state_dir, ...dependency, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
713
+ });
714
+ if (typeof intent.control_id !== "string")
715
+ throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
716
+ technicalPauseRequested = true;
717
+ const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
718
+ throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
719
+ };
720
+ const confirmStopped = async (stateDir, receipt) => {
721
+ const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
722
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
723
+ if (!binding.state.daemon_id || receipt.stopped !== true || !nativeShutdownConfirmed(receipt, { daemonId: binding.state.daemon_id }))
724
+ throw fail();
725
+ const deadline = performance.now() + 30_000;
726
+ while (processTreeIsAlive(binding.process) && performance.now() < deadline)
727
+ await delay(100);
728
+ const current = readNativeDaemonState(stateDir);
729
+ if (processTreeIsAlive(binding.process) || current.shutdown_state !== "clean"
730
+ || current.pid !== binding.process.pid || (current.daemon_id ?? current.config?.daemonId) !== (binding.state.daemon_id ?? binding.state.config?.daemonId)
731
+ || current.resource_process?.id !== binding.process.id || current.resource_process?.lease_token !== binding.process.lease_token)
732
+ throw fail();
733
+ };
734
+ const stopSession = async (session) => {
735
+ if (session.stopped)
736
+ return;
737
+ const receipt = await call(session, ["daemon", "stop", "--state-dir", session.state_dir, "--json"], false);
738
+ await confirmStopped(session.state_dir, receipt);
739
+ session.stopped = true;
740
+ saveState(context, row, state);
741
+ appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
742
+ };
743
+ const retireSession = async (session) => {
744
+ session.retired = true;
745
+ saveState(context, row, state);
746
+ try {
747
+ await stopSession(session);
748
+ session.cleanup_pending = false;
749
+ note(session, "native_session_cleanup_pending", "native_cleanup", "none", undefined, "resolved");
750
+ return true;
751
+ }
752
+ catch {
753
+ session.cleanup_pending = true;
754
+ saveState(context, row, state);
755
+ note(session, "native_session_cleanup_pending", "native_cleanup", "cleanup_pending");
756
+ // Full cleanup remains truthful. Only physical absence or an already
757
+ // accepted native execution-settlement proof permits conflicting reuse.
758
+ try {
759
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
760
+ if (managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id }))
761
+ return true;
762
+ return managedExecutionReusable(binding.state, runtimeHarness(session.profile));
763
+ }
764
+ catch {
765
+ return false;
766
+ }
767
+ }
768
+ };
769
+ const settle = async () => {
770
+ await Promise.all(state.sessions.map(retireSession));
771
+ };
772
+ const captureBoundary = async (stage, nextStage) => {
773
+ try {
774
+ await captureControllerBoundary(executionContext, row, manifest, stage, nextStage, assertPhysicalOwner);
775
+ }
776
+ catch (error) {
777
+ const code = String(error?.code ?? "capture_unavailable");
778
+ if (["controller_owner_mismatch", "controller_capture_owner_lost", "run_recovery_guarded", "run_recovery_generation_stale"].includes(code))
779
+ throw error;
780
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "boundary_capture_unavailable", message: "The Stage result is accepted; its assessment snapshot is unavailable.", phase: "assessment_capture", generation: row.generation, stage: stage.stage, ...(manifest.request.capture_root ? { diagnostic_reference: manifest.request.capture_root } : {}), consequence: "none" });
781
+ context.db.run("UPDATE run_controllers SET status='running',last_error_json=NULL,updated_at=? WHERE controller_id=? AND owner_token=? AND generation=? AND status IN ('capturing','capture_failed')", [context.now(), row.controller_id, row.owner_token, row.generation]);
782
+ // A missing report is not a failed Stage. A real unstable source still
783
+ // fences its dependent successor until the existing control can seal it.
784
+ if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
785
+ await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
786
+ }
787
+ };
788
+ const reconcilePromptPredecessor = async (session) => {
789
+ const prior = state.last_receipt?.settlement;
790
+ const accepted = state.last_receipt?.accepted_boundary;
791
+ if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
792
+ return;
793
+ const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
794
+ if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
795
+ await pauseDependency(session, undefined, error, "predecessor_settlement");
796
+ throw error;
797
+ });
798
+ state.last_receipt = receipt;
799
+ saveState(context, row, state);
800
+ const current = receipt.settlement;
801
+ if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
802
+ if (accepted && controllerObligationAccepted(context, row, accepted)) {
803
+ note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
804
+ if (receipt.native_settled === true && receipt.settled === true)
805
+ return;
806
+ await pauseDependency(session, undefined, new AppError("accepted_boundary_settlement_pending", "Accepted boundary still has an unsettled native predecessor", 1), "predecessor_settlement");
807
+ }
808
+ throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
809
+ }
810
+ };
811
+ const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
812
+ const hitl = kind === "hitl";
813
+ const pending = state.pending_capacity;
814
+ if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
815
+ throw new AppError("capacity_continuation_conflict", "Retained capacity owner reference differs from its Session/task", 1);
816
+ if (!pending)
817
+ await reconcilePromptPredecessor(session);
818
+ const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
819
+ const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
820
+ if (expectedBoundary && failureResolution)
821
+ expectedBoundary.failure_resolution = failureResolution;
822
+ const hitlText = async () => {
823
+ const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
824
+ const answer = context.db.get("SELECT input_json,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status IN ('accepted','completed')", [answerId, row.controller_id]);
825
+ const accepted = answer && JSON.parse(answer.input_json);
826
+ const packet = answer?.receipt_json && JSON.parse(answer.receipt_json).hitl_packet;
827
+ if (!packet || !accepted || packet.generation !== row.generation || packet.pause_id !== accepted.pause_id || packet.stage !== accepted.stage || packet.work_id !== accepted.work_id || packet.answer_file !== accepted.file || packet.answer_sha256 !== accepted.sha256)
828
+ throw new AppError("controller_hitl_packet_invalid", "Accepted HITL continuation lost its exact binding", 1);
829
+ const issued = context.db.get("SELECT id,status,scope_json,command FROM lifecycle_invocations WHERE id = ?", [packet.invocation_id ?? null]);
830
+ if (!issued || issued.scope_json !== executionContext.env.DD_FLOW_INVOCATION_SCOPE)
831
+ throw new AppError("controller_hitl_packet_invalid", "Accepted HITL command belongs to another scope", 1);
832
+ if (issued.status === "executing" || issued.status === "settled") {
833
+ const recovered = await awaitLifecycleInvocation(executionContext, { id: issued.id, command: issued.command, assertCurrent: scope => assertLifecycleInvocationCurrent(executionContext, scope, issued.command) });
834
+ if (!recovered.replay)
835
+ throw new AppError("controller_hitl_reconciliation_required", "Accepted resume is not committed", 1);
836
+ const outcome = recovered.outcome;
837
+ if (!outcome.result)
838
+ throw new AppError("controller_hitl_reconciliation_required", "Accepted resume has no committed continuation", 1);
839
+ if (outcome.result.resume_source_path !== packet.resume_source_path || typeof outcome.result.answer_path !== "string")
840
+ throw new AppError("controller_hitl_packet_invalid", "Committed resume differs from the accepted packet locator", 1);
841
+ publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, outcome.result.answer_path);
842
+ publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, packet.resume_source_path);
843
+ const bytes = fs.readFileSync(outcome.result.answer_path);
844
+ if (digestBytes(bytes) !== packet.answer_sha256)
845
+ throw new AppError("context_checksum_mismatch", "Committed answer differs from its accepted bytes", 1);
846
+ fs.mkdirSync(path.dirname(packet.answer_file), { recursive: true, mode: 0o700 });
847
+ publishImmutableInput(packet.answer_file, bytes, "hitl_answer_conflict");
848
+ publishPromptDelivery(outcome.result);
849
+ const delivery = outcome.result.prompt_delivery;
850
+ const promptPath = delivery?.path ?? outcome.result.prompt_path;
851
+ if (typeof promptPath !== "string" || !path.isAbsolute(promptPath))
852
+ throw new AppError("controller_hitl_packet_invalid", "Committed resume has no absolute prompt delivery", 1);
853
+ // Keep continuation bytes stable across a retained overload episode.
854
+ // Its conditional instructions point at this restored exact custody.
855
+ return controllerStageResumePrompt(packet);
856
+ }
857
+ if (issued.status !== "issued" || digestBytes(fs.readFileSync(packet.answer_file)) !== packet.answer_sha256)
858
+ throw new AppError("context_checksum_mismatch", "Accepted HITL answer or command changed before resume", 1);
859
+ return controllerStageResumePrompt(packet);
860
+ };
861
+ const send = (value, id) => {
862
+ const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
863
+ if (retained) {
864
+ const dispatch = JSON.parse(retained.input_json);
865
+ if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
866
+ throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
867
+ return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease, JSON.parse(retained.input_json).obligation);
868
+ }
869
+ const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
870
+ fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
871
+ state.turns += 1;
872
+ saveState(context, row, state);
873
+ return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease, expectedBoundary);
874
+ };
875
+ let receipt;
876
+ try {
877
+ try {
878
+ if (pending) {
879
+ const predecessor = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'failed'", [pending.predecessor, row.controller_id, row.generation]);
880
+ if (!predecessor?.error_json)
881
+ throw new AppError("capacity_continuation_conflict", "Retained capacity predecessor has no failure receipt", 1);
882
+ // The root identifies the whole chain; inspect its frozen failure,
883
+ // then reconcile all retained successors before any dispatch.
884
+ const root = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [firstOperationId, row.controller_id, row.generation]);
885
+ if (!root?.error_json)
886
+ throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
887
+ throw JSON.parse(root.error_json);
888
+ }
889
+ const failures = failedChildren(session);
890
+ if (failures.length && !(kind === "child" && childCapacityDispatchAllowed(state, failures, session.id, firstOperationId, digestBytes(text))))
891
+ throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
892
+ promptPending = true;
893
+ observedPromptOperation = firstOperationId;
894
+ try {
895
+ receipt = await send(hitl ? await hitlText() : text, firstOperationId);
896
+ }
897
+ finally {
898
+ promptPending = false;
899
+ observedPromptOperation = null;
900
+ }
901
+ }
902
+ catch (error) {
903
+ const hitlContinuationText = hitl && terminalProviderOverload(error, session.id) ? await hitlText() : undefined;
904
+ const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
905
+ args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
906
+ const resumed = await continueProviderCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
907
+ retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
908
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent?.root_operation_id === firstOperationId ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
909
+ retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
910
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_root === firstOperationId && retained.capacity_failure ? [retained.capacity_failure] : []; }),
911
+ retainFailure: (id, refusal) => {
912
+ const changed = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_root: firstOperationId, capacity_failure: refusal }), context.now(), id, row.controller_id, row.generation]);
913
+ if (changed.changes !== 1)
914
+ throw new AppError("capacity_continuation_blocked", "Failed Turn owner changed before its refusal was retained", 1);
915
+ },
916
+ inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
917
+ inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
918
+ assertCurrent: () => {
919
+ assertOwner();
920
+ },
921
+ decision: async (boundary, currentFailure) => {
922
+ if (kind === "child") {
923
+ const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
924
+ const parent = normalizeRecoveryFailure(currentFailure, session.id);
925
+ const decision = childCapacityRecoveryDecision(state, controllerNativeChildren(seen, session.id), session.id, firstOperationId, parent?.request_proof.kind === "native_turn" ? parent.turn_id : undefined);
926
+ if (decision !== "continue") {
927
+ saveState(context, row, state);
928
+ return decision;
929
+ }
930
+ }
931
+ const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
932
+ const next = view.continuation;
933
+ if (boundary) {
934
+ if (!ownedProviderBoundary(context, { projectId: row.project_id, runId: row.run_id, stage: state.stage, sessionId: session.id, generation: row.generation, scope: "stage" }, boundary))
935
+ return "reconciliation_blocked";
936
+ return boundary.outcome === "paused" ? "paused" : "completed";
937
+ }
938
+ if (next.kind === "blocked")
939
+ return "reconciliation_blocked";
940
+ if (next.kind === "paused" && next.stage !== state.stage)
941
+ return "paused";
942
+ if (next.kind === "terminal" || next.stage !== state.stage)
943
+ return "completed";
944
+ if (hitl) {
945
+ const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
946
+ const answer = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status = 'accepted'", [answerId, row.controller_id]);
947
+ const accepted = answer && JSON.parse(answer.input_json);
948
+ const pause = view.index.stage_runs.find(item => item.stage === accepted?.stage)?.pause;
949
+ if (next.kind !== "paused")
950
+ return "continue";
951
+ if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
952
+ return "paused";
953
+ }
954
+ else if (next.kind === "paused")
955
+ return "paused";
956
+ return next.kind === "wait" && !["native_children_unsettled", "work_receipts_pending", "qualified_capacity_required"].includes(next.reason) ? "reconciliation_blocked" : "continue";
957
+ },
958
+ retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
959
+ prompt: (id, value) => send(value, id),
960
+ ...(hitlContinuationText ? { continuationText: hitlContinuationText } : {}) });
961
+ if (!resumed)
962
+ throw error;
963
+ receipt = resumed;
964
+ }
965
+ checkOutcomes();
966
+ await childObserver(session)?.drain();
967
+ childObserver(session)?.completePostTurn(receipt);
968
+ const observationGap = childObserver(session)?.gap();
969
+ if (observationGap) {
970
+ if (expectedBoundary && controllerObligationAccepted(context, row, expectedBoundary))
971
+ note(session, "accepted_boundary_observation_gap", "native_observation", "cleanup_pending", firstOperationId);
972
+ else
973
+ throw new AppError("operation_observation_lost", "In-flight child observation remained unavailable at the safe Turn boundary", 1, { observation_error_code: observationGap, provider_session_id: session.id, stage: state.stage });
974
+ }
975
+ if (fatal)
976
+ throw fatal;
977
+ }
978
+ catch (error) {
979
+ if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
980
+ throw error;
981
+ if (!fatal && lateNativeFailure(error, session.id)) {
982
+ if (failureResolution && failureDecisionReady(failureResolution))
983
+ acceptFailureDecision(executionContext, row, failureResolution, assertOwner);
984
+ await pauseDependency(session, firstOperationId, error, "productive_continuation");
985
+ }
986
+ await stopAfterFailure(error);
987
+ if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
988
+ cancellation.abort();
989
+ throw fatal ?? error;
990
+ }
991
+ state.last_receipt = receipt;
992
+ delete state.pending_capacity;
993
+ saveState(context, row, state);
994
+ return receipt;
995
+ };
996
+ const monitor = setInterval(() => {
997
+ if (fatal || technicalPauseRequested)
998
+ return;
999
+ try {
1000
+ checkOutcomes();
1001
+ if (promptPending && state.current_session !== null)
1002
+ childObserver(state.sessions[state.current_session])?.tick();
1003
+ }
1004
+ catch (error) {
1005
+ void stopAfterFailure(error);
1006
+ }
1007
+ }, 1000);
1008
+ monitor.unref();
1009
+ const resolutionWriterSettled = async (session) => {
1010
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1011
+ state.last_receipt = inspected;
1012
+ saveState(context, row, state);
1013
+ return inspected.settled === true && !controllerNativeChildren(inspected, session.id).some(child => ["running", "unknown"].includes(child.status));
1014
+ };
1015
+ try {
1016
+ for (;;) {
1017
+ if (technicalPauseRequested)
1018
+ return;
1019
+ // Renewal touches the resource registry; never do it from a callback that
1020
+ // may already own the RUN writer.
1021
+ await renewLease();
1022
+ assertOwner();
1023
+ if (state.current_session !== null && !state.sessions[state.current_session].stopped)
1024
+ bindInvocationScope(state.sessions[state.current_session]);
1025
+ const run = requireRun(context, row.project_id, row.run_id);
1026
+ const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
1027
+ const next = view.continuation;
1028
+ terminalFinalization = next.kind === "terminal";
1029
+ if (state.pending_capacity && next.kind !== "terminal" && !["done", "failed", "cancelled"].includes(run.status)) {
1030
+ const pending = state.pending_capacity;
1031
+ const session = state.sessions.find(item => item.id === pending.session_id && !item.stopped);
1032
+ const operation = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [pending.root_operation_id, row.controller_id, row.generation]);
1033
+ const dispatch = operation && JSON.parse(operation.input_json);
1034
+ const args = dispatch?.args;
1035
+ const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
1036
+ if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
1037
+ throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
1038
+ await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind, dispatch?.obligation?.action ?? "finish", dispatch?.obligation?.work_ids, dispatch?.obligation?.failure_resolution);
1039
+ continue;
1040
+ }
1041
+ const failureRecovery = await reconcilePendingWorkFailure(executionContext, row, state, next.kind, run.project_root, assertOwner, resolutionWriterSettled);
1042
+ if (failureRecovery === "waiting") {
1043
+ setStatus("waiting_for_children");
1044
+ await delay(1000);
1045
+ continue;
1046
+ }
1047
+ if (failureRecovery === "applied") {
1048
+ saveState(context, row, state);
1049
+ continue;
1050
+ }
1051
+ if (next.kind !== "paused" && state.current_session !== null) {
1052
+ const committed = context.db.get("SELECT operation_id,receipt_json FROM run_controller_operations WHERE controller_id = ? AND status = 'accepted' AND json_extract(input_json,'$.action') = 'user_answer' AND json_extract(receipt_json,'$.hitl_packet.generation') = ? AND json_extract(receipt_json,'$.hitl_packet.stage') = ? ORDER BY created_at LIMIT 1", [row.controller_id, row.generation, state.stage]);
1053
+ if (committed) {
1054
+ const packet = JSON.parse(committed.receipt_json).hitl_packet;
1055
+ const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${committed.operation_id}:prompt:${row.generation}`, "hitl");
1056
+ context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [adapterSessionId(receipt), context.now(), committed.operation_id]);
1057
+ state.last_continuation = null;
1058
+ saveState(context, row, state);
1059
+ continue;
1060
+ }
1061
+ }
1062
+ const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
1063
+ if (state.current_session !== null && (completed || next.stage !== state.stage)) {
1064
+ const session = state.sessions[state.current_session], failures = failedChildren(session);
1065
+ if (failures.length && !completed)
1066
+ throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids advancing an unaccepted Stage", 1);
1067
+ // The Stage gate is authoritative even for a native helper with no Work
1068
+ // association. This does not claim that helper succeeded or stopped:
1069
+ // normal Session retirement/predecessor reconciliation still fences writes.
1070
+ for (const child of failures)
1071
+ note(session, "accepted_stage_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
1072
+ }
1073
+ if (completed && manifest.request.stop_after === state.stage) {
1074
+ // A bounded run is final evidence, not an entry fixture for its successor.
1075
+ // Stop owned observers before capture; keep the controller lease/fence.
1076
+ await settle();
1077
+ if (manifest.request.capture_root)
1078
+ await captureBoundary(completed, null);
1079
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
1080
+ setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
1081
+ return;
1082
+ }
1083
+ if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
1084
+ const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
1085
+ if (key !== lastCapture) {
1086
+ await captureBoundary(completed, next.kind === "start_stage" ? next.stage : null);
1087
+ lastCapture = key;
1088
+ }
1089
+ }
1090
+ if (next.kind === "terminal") {
1091
+ await settle();
1092
+ setStatus(run.status === "cancelled" ? "cancelled" : "completed");
1093
+ appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
1094
+ return;
1095
+ }
1096
+ if ((next.kind === "paused" || next.kind === "blocked") && workFailureBlockActive(executionContext, row, view.index)) {
1097
+ setStatus("waiting_for_dependency");
1098
+ await delay(1000, undefined, { signal: cancellation.signal });
1099
+ continue;
1100
+ }
1101
+ if (next.kind === "paused") {
1102
+ setStatus("waiting_for_user");
1103
+ const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
1104
+ if (!pause || state.current_session === null)
1105
+ throw new AppError("controller_hitl_missing", "Paused RUN has no managed Session or HITL receipt", 1);
1106
+ const answer = context.db.get("SELECT operation_id, input_json, status FROM run_controller_operations WHERE controller_id = ? AND status IN ('accepted','completed') AND json_extract(input_json, '$.action') = 'user_answer' AND json_extract(input_json, '$.pause_id') = ? ORDER BY created_at LIMIT 1", [row.controller_id, pause.id]);
1107
+ if (!answer) {
1108
+ await delay(1000);
1109
+ continue;
1110
+ }
1111
+ if (answer.status === "completed")
1112
+ throw new AppError("controller_answer_not_applied", "Completed answer operation still has its original HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage });
1113
+ const input = JSON.parse(answer.input_json);
1114
+ if (digestBytes(fs.readFileSync(input.file)) !== input.sha256)
1115
+ throw new AppError("context_checksum_mismatch", "Accepted HITL answer bytes changed", 1);
1116
+ await renewLease();
1117
+ assertOwner();
1118
+ // The accepted answer is logical intent; only its generation-bound
1119
+ // adapter operation represents a physical dispatch that stop must drain.
1120
+ let invocationId;
1121
+ const capture = executionContext.captureLifecycleAssignment;
1122
+ executionContext.captureLifecycleAssignment = id => { invocationId = id; capture?.(id); };
1123
+ let command;
1124
+ try {
1125
+ command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${quote([run.project_root])} --answer-file ${quote([input.file])} --json`, { replaceUnobservedAnswer: true });
1126
+ }
1127
+ finally {
1128
+ if (capture)
1129
+ executionContext.captureLifecycleAssignment = capture;
1130
+ else
1131
+ delete executionContext.captureLifecycleAssignment;
1132
+ }
1133
+ if (!invocationId)
1134
+ throw new AppError("controller_hitl_packet_invalid", "Accepted resume did not capture its issued identity", 1);
1135
+ const packet = { pause_id: pause.id, stage: next.stage, work_id: input.work_id, answer_file: input.file, answer_sha256: input.sha256, command, generation: row.generation, invocation_id: invocationId, resume_source_path: stageResumeSourcePath(pause, fs.readFileSync(input.file, "utf8")) };
1136
+ context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [JSON.stringify({ hitl_packet: packet }), context.now(), answer.operation_id]);
1137
+ const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${answer.operation_id}:prompt:${row.generation}`, "hitl");
1138
+ const afterAnswer = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
1139
+ if (afterAnswer.index.stage_runs.some(stage => stage.status === "paused" && stage.pause?.id === pause.id)) {
1140
+ throw new AppError("controller_answer_not_applied", "The answer Turn completed without resuming the assigned HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage, effect: "no_progress" });
1141
+ }
1142
+ context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ?", [adapterSessionId(receipt), context.now(), answer.operation_id]);
1143
+ state.last_continuation = null;
1144
+ saveState(context, row, state);
1145
+ setStatus("running");
1146
+ continue;
1147
+ }
1148
+ if (next.kind === "blocked" || next.kind === "wait" || !next.stage)
1149
+ throw new AppError("controller_continuation_blocked", "RUN has no legal managed continuation", 1, { continuation: next });
1150
+ if (next.kind === "start_stage") {
1151
+ // Only context slices can be added; profiles and launch inputs stay frozen.
1152
+ manifest.contexts = JSON.parse(requireController(context, row.controller_id).manifest_json).contexts;
1153
+ const stageContext = manifest.contexts[next.stage];
1154
+ if (!stageContext) {
1155
+ if (requireController(context, row.controller_id).status !== "waiting_for_context")
1156
+ appendEvent(context, row.controller_id, "context_required", { stage: next.stage, attempt: next.attempt ?? null });
1157
+ setStatus("waiting_for_context");
1158
+ await delay(1000);
1159
+ continue;
1160
+ }
1161
+ loadExternalStageContext({ file: stageContext.file, expectedSha256: stageContext.sha256, stage: next.stage });
1162
+ const continuation = JSON.stringify(["start_stage", next.stage, next.attempt ?? null]);
1163
+ retainControllerContinuation(state, continuation, { code: "stage_entry_nonprogressing", message: "The coordinator returned without starting the assigned Stage", details: { stage: next.stage, attempt: next.attempt ?? null, last_receipt: state.last_receipt ?? null } });
1164
+ const transition = routing.transition(next.stage, state.stage ?? undefined, run);
1165
+ if (routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "server" && !state.merge_claim) {
1166
+ const request = context.db.get("SELECT merge_request_id, status FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, merge_request_id DESC LIMIT 1", [row.project_id, row.run_id]);
1167
+ if (!request || request.status !== "queued")
1168
+ throw new AppError("controller_attach_required", "Managed MERGE needs an unclaimed queued request; reconcile an existing owner", 1, { request });
1169
+ const claim = claimQueuedMergeRequest(context, { requestId: request.merge_request_id, ownerId: row.controller_id, expectedGeneration: row.generation });
1170
+ if (!claim) {
1171
+ setStatus("waiting_for_merge");
1172
+ await delay(1000);
1173
+ continue;
1174
+ }
1175
+ state.merge_claim = { request_id: claim.request.merge_request_id, token: claim.token, target_workspace: claim.request.target_workspace, work_id: claim.request.executor_work_id };
1176
+ saveState(context, row, state);
1177
+ appendEvent(context, row.controller_id, "merge_claimed", { request_id: claim.request.merge_request_id });
1178
+ await renewLease();
1179
+ }
1180
+ const stageCwd = routing.cwd(next.stage, state.merge_claim?.target_workspace, run);
1181
+ const routeBinding = (current) => {
1182
+ const index = JSON.parse(current.index_json);
1183
+ return JSON.stringify([current.project_root, current.workspace_root, index.workspace_route ?? null, state.merge_claim ?? null]);
1184
+ };
1185
+ const preparedRoute = routeBinding(run);
1186
+ const assertRoute = async () => {
1187
+ assertOwner();
1188
+ if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
1189
+ const current = state.current_session === null ? null : state.sessions[state.current_session];
1190
+ if (current && !current.stopped)
1191
+ await retireSession(current);
1192
+ throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
1193
+ }
1194
+ };
1195
+ await assertRoute();
1196
+ let session = state.current_session === null ? null : state.sessions[state.current_session];
1197
+ if (session && routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "same_session" && path.resolve(session.cwd) !== stageCwd) {
1198
+ throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
1199
+ }
1200
+ if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
1201
+ if (session && !await retireSession(session))
1202
+ await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
1203
+ await renewLease();
1204
+ assertOwner();
1205
+ const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
1206
+ fs.mkdirSync(directory, { mode: 0o700 });
1207
+ const owner = { schema_id: "dd-flow/runtime-owner@1", owner_id: row.controller_id, role: "subject", project_id: row.project_id, run_id: row.run_id, operation_id: `${row.controller_id}:session:${state.sessions.length + 1}`, state_dir: directory, dd_flow_home: context.ddFlowHome, dd_flow_bin: manifest.flow_executable, resource_home: manifest.resource_home, budget: manifest.budget };
1208
+ if (next.stage === "merge" && state.merge_claim) {
1209
+ owner.role = "merge";
1210
+ owner.work_id = state.merge_claim.work_id;
1211
+ }
1212
+ session = { id: "", profile: transition.coordinator, state_dir: directory, cwd: stageCwd, journal: path.join(directory, "adapter.events.jsonl"), owner };
1213
+ if (runtimeHarness(session.profile) === "codex-desktop") {
1214
+ session.codex_home = path.join(directory, "codex-home");
1215
+ initCodexHome(context, { projectRoot: run.project_root, profile: `controller-${digest(directory).slice(0, 16)}`, targetHome: session.codex_home, ...(context.env.CODEX_HOME ? { sourceHome: context.env.CODEX_HOME } : {}) });
1216
+ }
1217
+ state.sessions.push(session);
1218
+ state.current_session = state.sessions.length - 1;
1219
+ saveState(context, row, state);
1220
+ await call(session, ["daemon", "start", ...sessionArguments(context, manifest, session)]);
1221
+ const created = await call(session, ["session", "create", ...sessionArguments(context, manifest, session)]);
1222
+ const sessionId = adapterSessionId(created);
1223
+ if (!sessionId)
1224
+ throw new AppError("harness_adapter_invalid", "Managed adapter did not return its Session id", 1);
1225
+ session.id = sessionId;
1226
+ saveState(context, row, state);
1227
+ bindInvocationScope(session);
1228
+ if (next.stage === "merge" && state.merge_claim)
1229
+ context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify({ controller_id: row.controller_id, profile: session.profile, session_id: session.id, state_dir: session.state_dir, create: created }), context.now(), state.merge_claim.request_id, row.controller_id, state.merge_claim.token]);
1230
+ appendEvent(context, row.controller_id, "session_created", { stage: next.stage, session_id: sessionId, harness: runtimeHarness(session.profile), profile: session.profile });
1231
+ }
1232
+ state.native_baseline = controllerNativeChildren(state.last_receipt ?? {}, session.id).map(child => child.session_id);
1233
+ state.stage = next.stage;
1234
+ state.child_capacity = {};
1235
+ saveState(context, row, state);
1236
+ setStatus("running");
1237
+ if (routing.kind === "sdlc" && (next.stage === "code" || next.stage === "code-review")) {
1238
+ // A CODE_REVIEW fork has source files but intentionally no installed
1239
+ // dependencies. Its repair Work executes checks just like CODE Work.
1240
+ const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
1241
+ await renewLease();
1242
+ assertOwner();
1243
+ appendEvent(context, row.controller_id, "code_workspace_prepared", { stage: next.stage, readiness_status: readiness.status ?? null });
1244
+ }
1245
+ const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
1246
+ const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
1247
+ bindControllerStageAssignment(executionContext, command, { controllerId: row.controller_id, sessionId: session.id, stateDir: session.state_dir, stage: next.stage, contextSha256: stageContext.sha256, ...(typeof next.attempt === "number" ? { attempt: next.attempt } : {}) });
1248
+ await assertRoute();
1249
+ appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
1250
+ await prompt(session, controllerStageEntryPrompt(next.stage, command), undefined, undefined, "entry");
1251
+ continue;
1252
+ }
1253
+ if (state.current_session === null)
1254
+ throw new AppError("controller_attach_required", "An unfinished Stage has no managed coordinator", 1);
1255
+ const session = state.sessions[state.current_session];
1256
+ const childHarness = runtimeHarness(session.profile);
1257
+ const baseline = new Set(state.native_baseline ?? []);
1258
+ for (const intent of Object.values(state.child_capacity ?? {})) {
1259
+ if (!intent.dispatched || !intent.operation_id || intent.parent_turn_id || !intent.prompt_sha256)
1260
+ continue;
1261
+ let saved = context.db.get("SELECT input_json,receipt_json,status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [intent.operation_id, row.controller_id, row.generation]);
1262
+ if (!saved)
1263
+ continue;
1264
+ const dispatch = JSON.parse(saved.input_json);
1265
+ if (!saved.receipt_json && saved.status === "outcome_unknown" && dispatch.dispatch_phase !== "prepared" && dispatch.prompt_sha256 === intent.prompt_sha256 && Array.isArray(dispatch.args)) {
1266
+ // Existing adapter recovery reads its durable operation; it cannot
1267
+ // redispatch a productive operation past external_dispatch_possible.
1268
+ await controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, intent.operation_id, cancellation.signal, renewLease);
1269
+ saved = context.db.get("SELECT input_json,receipt_json,status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [intent.operation_id, row.controller_id, row.generation]);
1270
+ }
1271
+ if (!saved?.receipt_json)
1272
+ continue;
1273
+ const receipt = JSON.parse(saved.receipt_json);
1274
+ if ((saved.status === "completed" || dispatch.dispatch_phase === "receipt_observed") && dispatch.prompt_sha256 === intent.prompt_sha256
1275
+ && receipt.provider_session_id === session.id && receipt.turn?._meta?.zcode?.ddFlowRecovery?.operation_id === intent.operation_id
1276
+ && typeof receipt.turn._meta.zcodeNativeTurnId === "string" && receipt.turn._meta.zcodeNativeTurnId) {
1277
+ intent.parent_turn_id = receipt.turn._meta.zcodeNativeTurnId;
1278
+ saveState(context, row, state);
1279
+ }
1280
+ }
1281
+ let children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
1282
+ if (reconcileChildCapacity(state, children, session.id))
1283
+ saveState(context, row, state);
1284
+ childObserver(session)?.observeSettledChildren(children);
1285
+ const retainedFailures = failedChildren(session);
1286
+ children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
1287
+ const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
1288
+ if (closedChildEpisodes.length)
1289
+ await prepareMutation(() => {
1290
+ saveState(context, row, state);
1291
+ appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
1292
+ });
1293
+ const queuedChild = children.find(child => !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
1294
+ && (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
1295
+ || child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
1296
+ if (queuedChild) {
1297
+ const observedAt = Date.parse(state.child_capacity[queuedChild.session_id].last_signal_at ?? "");
1298
+ if (!Number.isFinite(observedAt) || Date.now() - observedAt >= 600_000)
1299
+ throw new AppError("operation_observation_lost", "Accepted native child continuation has no new observed signal", 1, { child_session_id: queuedChild.session_id });
1300
+ setStatus("waiting_for_children");
1301
+ await delay(1000, undefined, { signal: cancellation.signal });
1302
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1303
+ saveState(context, row, state);
1304
+ continue;
1305
+ }
1306
+ const recoveryUnavailable = children.map(child => {
1307
+ const links = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: child.session_id })]);
1308
+ return links.length > 1 || links.some(link => link.status !== "running") ? null : unavailableProviderChildRecovery(child, session.id);
1309
+ }).find(Boolean);
1310
+ if (recoveryUnavailable) {
1311
+ if (children.every(child => ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status)))
1312
+ throw recoveryUnavailable;
1313
+ // A blocked child does not cancel healthy siblings. Observe their own
1314
+ // native completion before reporting the unavailable continuation.
1315
+ setStatus("waiting_for_children");
1316
+ await delay(1000, undefined, { signal: cancellation.signal });
1317
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1318
+ saveState(context, row, state);
1319
+ continue;
1320
+ }
1321
+ if (children.some(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && renderNativeChildFollowupInstruction(childHarness, child))) {
1322
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1323
+ state.last_receipt = inspected;
1324
+ saveState(context, row, state);
1325
+ children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1326
+ const failed = children.filter(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1327
+ // Freeze each distinct child refusal on its first exact observation,
1328
+ // even while a healthy sibling still owns an active native Turn.
1329
+ for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
1330
+ const linked = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: failedChild.session_id })]);
1331
+ if (linked.length > 1 || linked.some(link => link.status !== "running"))
1332
+ continue;
1333
+ const retained = state.child_capacity?.[failedChild.session_id];
1334
+ if (retained?.policy && retained.policy !== CAPACITY_POLICY)
1335
+ throw new AppError("capacity_policy_mismatch", "Retained child episode requires its original pinned recovery policy", 1);
1336
+ const refusals = [...(retained?.refusals ?? [])];
1337
+ if (refusals.some(item => item.turn_id === failedChild.terminal_turn_id))
1338
+ continue;
1339
+ const nativeFailure = providerChildFailure(failedChild);
1340
+ refusals.push(refusalObservation(nativeFailure, retained?.attempts ?? 0, failedChild.terminal_observed_at ?? context.now()));
1341
+ state.child_capacity ??= {};
1342
+ state.child_capacity[failedChild.session_id] = { ...retained, turn_id: retained?.turn_id ?? failedChild.terminal_turn_id, attempts: retained?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
1343
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_refusal", { child_session_id: failedChild.session_id, refusal: refusals.at(-1) }); });
1344
+ const exhausted = overloadExhausted(refusals);
1345
+ if (exhausted)
1346
+ throw new AppError("provider_unavailable", "Native child exhausted its provider continuation attempts", 1, { ...exhausted, child_session_id: failedChild.session_id, primary_error: nativeFailure });
1347
+ }
1348
+ // Continue one existing child at a time; another failed child does not
1349
+ // create a second fanout wave or share this child's refusal counter.
1350
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => renderNativeChildFollowupInstruction(childHarness, item) && eligibleProviderChildOverload(item, session.id)) : undefined;
1351
+ const turnId = child?.terminal_turn_id;
1352
+ const native = child && turnId ? providerChildFailure(child) : null;
1353
+ const prior = child && state.child_capacity?.[child.session_id];
1354
+ const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: child.session_id })]) : [];
1355
+ if (child && turnId && native && terminalProviderOverload(native, child.session_id, turnId)
1356
+ && nativeItemsSettled(child.native_turn_items)
1357
+ && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
1358
+ && (!links.length || links[0].status === "running")) {
1359
+ const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
1360
+ if (prior?.turn_id === turnId && prior.dispatched === true && !prepared)
1361
+ throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
1362
+ const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
1363
+ if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
1364
+ throw new AppError("capacity_continuation_conflict", "Native child continuation policy or ordinal is incompatible", 1);
1365
+ const refusals = [...(prior?.refusals ?? [])];
1366
+ if (!refusals.some(item => item.turn_id === turnId)) {
1367
+ const refusal = refusalObservation(native, attempt - 1, child.terminal_observed_at ?? context.now());
1368
+ refusals.push(refusal);
1369
+ state.child_capacity ??= {};
1370
+ state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
1371
+ saveState(context, row, state);
1372
+ }
1373
+ const exhausted = overloadExhausted(refusals);
1374
+ if (exhausted)
1375
+ throw new AppError("provider_unavailable", "Native child exhausted its provider continuation attempts", 1, { ...exhausted, child_session_id: child.session_id, primary_error: native });
1376
+ let wait;
1377
+ try {
1378
+ wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
1379
+ }
1380
+ catch (error) {
1381
+ const failure = error;
1382
+ throw new AppError(failure.code ?? "capacity_retry_metadata_invalid", failure.message ?? "Invalid child retry delay", 1, { ...failure.details, child_session_id: child.session_id, root_operation_id: prior?.root_operation_id ?? null, attempts: refusals.length, primary_error: native });
1383
+ }
1384
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
1385
+ throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
1386
+ const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
1387
+ const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
1388
+ state.child_capacity ??= {};
1389
+ const followupText = renderNativeChildFollowupInstruction(childHarness, child);
1390
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false, phase: "prepare", prompt_sha256: digestBytes(followupText), ...(child.continuation_ref ? { continuation_ref: child.continuation_ref } : {}) };
1391
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId }); });
1392
+ const until = performance.now() + wait;
1393
+ while (performance.now() < until) {
1394
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
1395
+ await renewLease();
1396
+ assertOwner();
1397
+ }
1398
+ await renewLease();
1399
+ assertOwner();
1400
+ const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
1401
+ if (current.continuation.stage !== state.stage || ["terminal", "paused"].includes(current.continuation.kind))
1402
+ continue;
1403
+ if (["blocked", "wait"].includes(current.continuation.kind))
1404
+ throw new AppError("capacity_child_followup_blocked", "RUN changed during native child capacity wait", 1, { child_session_id: child.session_id });
1405
+ const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1406
+ const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
1407
+ const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: child.session_id })]);
1408
+ if (currentLinks.length === 1 && ["completed", "cancelled", "paused"].includes(currentLinks[0].status)) {
1409
+ state.last_receipt = fresh;
1410
+ saveState(context, row, state);
1411
+ continue;
1412
+ }
1413
+ if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
1414
+ || !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
1415
+ || currentLinks.some(link => link.status !== "running"))
1416
+ throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
1417
+ await renewLease();
1418
+ assertOwner();
1419
+ const text = renderNativeChildFollowupInstruction(childHarness, child);
1420
+ // Fence dispatch before awaiting the parent: a lost ack must never
1421
+ // resend SendMessage into a child which may already be running.
1422
+ state.child_capacity[child.session_id].dispatched = true;
1423
+ state.child_capacity[child.session_id].phase = "dispatched";
1424
+ saveState(context, row, state);
1425
+ await prompt(session, text, operationId, "child");
1426
+ const parentTurnId = state.last_receipt?.turn?._meta?.zcodeNativeTurnId;
1427
+ if (typeof parentTurnId === "string" && parentTurnId)
1428
+ state.child_capacity[child.session_id].parent_turn_id = parentTurnId;
1429
+ state.child_capacity[child.session_id].dispatched = true;
1430
+ saveState(context, row, state);
1431
+ const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1432
+ const afterChildren = controllerNativeChildren(after, session.id);
1433
+ reconcileChildCapacity(state, afterChildren, session.id);
1434
+ if (after.descendants)
1435
+ after.descendants = afterChildren;
1436
+ state.last_receipt = after;
1437
+ saveState(context, row, state);
1438
+ const observed = controllerNativeChildren(after, session.id).find(item => item.session_id === child.session_id);
1439
+ if (!observed || observed.status === "failed" && observed.terminal_turn_id === turnId && state.child_capacity[child.session_id].phase !== "native_message_accepted")
1440
+ throw new AppError("capacity_child_followup_unconfirmed", "Native child did not advance to a new Turn after parent followup", 1, { child_session_id: child.session_id, turn_id: turnId });
1441
+ continue;
1442
+ }
1443
+ }
1444
+ const repairBlockedWorks = [];
1445
+ if (!retainedFailures.length && children.some(child => child.status === "completed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id))) {
1446
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1447
+ state.last_receipt = inspected;
1448
+ saveState(context, row, state);
1449
+ children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1450
+ // Do not compete with an active parent/healthy sibling or turn a known
1451
+ // repair into a replacement fanout wave.
1452
+ if (inspected.settled !== true || children.some(child => ["running", "unknown"].includes(child.status))) {
1453
+ setStatus("waiting_for_children");
1454
+ await delay(1000, undefined, { signal: cancellation.signal });
1455
+ continue;
1456
+ }
1457
+ let repair = null;
1458
+ const blockers = [];
1459
+ for (const child of children) {
1460
+ try {
1461
+ repair ??= completedChildRepair(executionContext, row, { stage: next.stage, harness: childHarness, parentId: session.id, children: [child] });
1462
+ }
1463
+ catch (error) {
1464
+ if (!(error instanceof AppError) || error.code !== "child_work_repair_blocked")
1465
+ throw error;
1466
+ blockers.push(error);
1467
+ }
1468
+ }
1469
+ if (repair) {
1470
+ reserveChildRepair(executionContext, row, repair, assertOwner);
1471
+ await prompt(session, repair.prompt, repair.operation_id, undefined, "work", [repair.work_id]);
1472
+ continue;
1473
+ }
1474
+ if (blockers.length) {
1475
+ const graph = getVnextFanoutStatus(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage }).orchestration;
1476
+ if (!graph?.works.ready.length)
1477
+ throw blockers[0];
1478
+ for (const blocker of blockers) {
1479
+ const workId = String(blocker.details.work_id);
1480
+ repairBlockedWorks.push(workId);
1481
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
1482
+ phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
1483
+ resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: JSON.stringify({
1484
+ protected_action: "work_finish", reason: blocker.details.reason, primary: blocker.details.primary_error,
1485
+ refusal_invocation_id: blocker.details.refusal_invocation_id, successor_invocation_id: blocker.details.successor_invocation_id,
1486
+ operation_id: blocker.details.operation_id
1487
+ }) });
1488
+ }
1489
+ }
1490
+ }
1491
+ const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
1492
+ const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0, repairBlockedWorks, ...(state.work_failure?.deferred ? { deferredFailure: state.work_failure.work_id } : {}) };
1493
+ const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
1494
+ : await nextControllerFanout(executionContext, fanoutInput);
1495
+ if (memoryFlow)
1496
+ await renewLease();
1497
+ assertOwner();
1498
+ if (fanout.kind === "none") {
1499
+ // An ACK-only recovery Turn may leave the original root Work unfinished.
1500
+ // Continue that retained packet once; never restart its Stage or repeat
1501
+ // an unknown productive dispatch merely because the observer reattached.
1502
+ const recovery = context.db.get(`SELECT o.operation_id,b.packet_json,b.packet_sha256,b.work_id
1503
+ FROM run_controller_operations o JOIN run_recovery_bindings b ON b.recovery_id=json_extract(o.receipt_json,'$.recovery_id')
1504
+ JOIN run_recovery_guards g ON g.recovery_id=b.recovery_id
1505
+ WHERE o.controller_id=? AND o.generation=? AND o.status='completed' AND json_extract(o.input_json,'$.action')='control_resume'
1506
+ AND b.status='accepted' AND g.status='recovered' AND g.project_id=? AND g.run_id=?
1507
+ AND b.provider_session_id=? AND b.daemon_id=json_extract(o.receipt_json,'$.daemon_id') ORDER BY o.created_at DESC LIMIT 1`, [row.controller_id, row.generation, row.project_id, row.run_id, session.id]);
1508
+ if (recovery && digestBytes(recovery.packet_json) === recovery.packet_sha256) {
1509
+ const packet = JSON.parse(recovery.packet_json);
1510
+ const operationId = `${recovery.operation_id}:unfinished-root`;
1511
+ const work = context.db.get('SELECT status FROM works WHERE work_id=? AND project_id=? AND run_id=? AND parent_work_id IS NULL', [recovery.work_id, row.project_id, row.run_id]);
1512
+ if (recoveryPacketMatchesStage(packet, { runId: row.run_id, generation: row.generation, stage: next.stage, workId: recovery.work_id }) && work?.status === 'running'
1513
+ && !context.db.get('SELECT 1 FROM run_controller_operations WHERE operation_id=?', [operationId])) {
1514
+ await prompt(session, `Continue the original unfinished root Work from this accepted recovery packet. Inspect retained receipts and partial effects first. Do not restart the Stage, repeat observed lifecycle operations, create another Work or start a successor Stage. Preserve the packet write scope and result schema; stop at its original boundary.\n\nAccepted recovery packet (JSON data):\n${recovery.packet_json}`, operationId);
1515
+ continue;
1516
+ }
1517
+ }
1518
+ assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1519
+ const cause = currentLifecycleFailure(executionContext, { runId: run.id, generation: row.generation, stage: next.stage, turn: state.turns, projectId: row.project_id, projectRoot: run.project_root });
1520
+ if (cause)
1521
+ throw new AppError(cause.code, cause.message, 1, { ...cause.details, ...(cause.invocation_id ? { invocation_id: cause.invocation_id } : {}), ...(cause.hook_event_id ? { hook_event_id: cause.hook_event_id } : {}), controller_wrapper: "incomplete_subject_turn", stage: next.stage });
1522
+ throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
1523
+ }
1524
+ if (fanout.kind === "failure") {
1525
+ const resolution = state.work_failure ?? prepareWorkFailureResolution(executionContext, row, next.stage, fanout.failure);
1526
+ if (resolution.work_id !== fanout.failure.work_id || resolution.work_session_id !== fanout.failure.work_session_id)
1527
+ throw new AppError("work_failure_scope_changed", "Pending resolution belongs to another failed attempt", 1);
1528
+ state.work_failure = resolution;
1529
+ saveState(context, row, state);
1530
+ while (!failureDecisionReady(resolution)) {
1531
+ const attempt = state.work_failure.submission_attempt ?? 0;
1532
+ if (attempt >= 2)
1533
+ await pauseDependency(session, undefined, new AppError("work_failure_decision_invalid", "Coordinator did not submit a valid bound failure decision", 1, workFailureDiagnostic(resolution)), "work_failure_resolution");
1534
+ await prompt(session, workFailureResolutionPrompt(resolution, state.work_failure.submission_diagnostic), `${row.controller_id}:failure-resolution:${digest([resolution.accepted_file, attempt, row.generation])}`, undefined, "failure_resolution", undefined, resolution);
1535
+ try {
1536
+ readFailureDecision(resolution);
1537
+ }
1538
+ catch (error) {
1539
+ state.work_failure.submission_diagnostic = error instanceof Error ? error.message : String(error);
1540
+ }
1541
+ state.work_failure.submission_attempt = attempt + 1;
1542
+ saveState(context, row, state);
1543
+ }
1544
+ const decision = acceptFailureDecision(executionContext, row, resolution, assertOwner);
1545
+ if (decision.action === "block" && fanout.ready) {
1546
+ state.work_failure.deferred = true;
1547
+ saveState(context, row, state);
1548
+ continue;
1549
+ }
1550
+ if (!await resolutionWriterSettled(session)) {
1551
+ setStatus("waiting_for_children");
1552
+ await delay(1000);
1553
+ continue;
1554
+ }
1555
+ applyWorkFailureDecision(executionContext, row, resolution, decision, run.project_root, assertOwner);
1556
+ delete state.work_failure;
1557
+ state.last_continuation = null;
1558
+ saveState(context, row, state);
1559
+ continue;
1560
+ }
1561
+ if (fanout.kind === "wait") {
1562
+ if (fanout.reason === "accepted_work_native_cleanup") {
1563
+ const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1564
+ for (const child of pending) {
1565
+ note(session, "accepted_work_native_child_pending", "native_completion", "cleanup_pending", child.session_id);
1566
+ try {
1567
+ await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
1568
+ }
1569
+ catch (error) {
1570
+ await pauseDependency(session, undefined, error, "accepted_child_retirement");
1571
+ }
1572
+ }
1573
+ const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
1574
+ const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
1575
+ if (lateNativeFailure(error, session.id))
1576
+ await pauseDependency(session, operationId, error, "accepted_child_retirement");
1577
+ throw error;
1578
+ });
1579
+ state.last_receipt = observed;
1580
+ saveState(context, row, state);
1581
+ if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
1582
+ await pauseDependency(session, operationId, new AppError("accepted_child_retirement_unconfirmed", "Accepted child results are retained but native retirement remains unconfirmed", 1), "accepted_child_retirement");
1583
+ continue;
1584
+ }
1585
+ setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
1586
+ await delay(1000);
1587
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1588
+ saveState(context, row, state);
1589
+ continue;
1590
+ }
1591
+ setStatus("running");
1592
+ if (fanout.kind === "external") {
1593
+ let cleanupConflict = false;
1594
+ const results = await Promise.allSettled(fanout.works.map(async (work) => {
1595
+ try {
1596
+ await renewLease();
1597
+ assertOwner();
1598
+ const receipt = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "external", input: { projectRoot: run.project_root, runId: run.id, workId: work.work_id, stage: next.stage } }, cancellation.signal)
1599
+ : await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
1600
+ if (memoryFlow) {
1601
+ await renewLease();
1602
+ assertOwner();
1603
+ }
1604
+ if (fatal)
1605
+ throw fatal;
1606
+ appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
1607
+ const launch = context.db.get("SELECT state_dir, profile_json FROM work_launches WHERE work_id = ?", [work.work_id]);
1608
+ const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
1609
+ if (!launch || !["completed", "failed", "cancelled"].includes(status ?? ""))
1610
+ throw new AppError("external_work_unsettled", "External launch returned without its terminal Work receipt; observe it rather than relaunching", 1, { work_id: work.work_id, status });
1611
+ const resolved = JSON.parse(launch.profile_json);
1612
+ const executable = manifest.adapters[runtimeHarness(resolved.worker)].executable;
1613
+ try {
1614
+ const stopped = await runHarnessAdapter({ executable, args: ["daemon", "stop", "--state-dir", launch.state_dir, "--json"], env: controllerEnvironment(context, manifest), operationId: `${row.controller_id}:${work.work_id}:settle` });
1615
+ await confirmStopped(launch.state_dir, stopped);
1616
+ appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
1617
+ }
1618
+ catch {
1619
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "external_work_cleanup_pending", message: "The Work outcome is committed; its native resources require retirement.", phase: "external_work_cleanup", generation: row.generation, stage: next.stage, work_id: work.work_id, harness: runtimeHarness(resolved.worker), resource_identity: launch.state_dir, diagnostic_reference: launch.state_dir, source_operation_id: `${row.controller_id}:${work.work_id}:settle`, consequence: "cleanup_pending" });
1620
+ try {
1621
+ cleanupConflict ||= !managedDaemonExecutionAbsent(executionContext, { stateDir: launch.state_dir, projectId: row.project_id, runId: row.run_id });
1622
+ }
1623
+ catch {
1624
+ cleanupConflict = true;
1625
+ }
1626
+ }
1627
+ }
1628
+ catch (error) {
1629
+ if (!(error instanceof AppError && error.code === "controller_technical_dependency_paused"))
1630
+ await stopAfterFailure(error);
1631
+ throw error;
1632
+ }
1633
+ }));
1634
+ const rejected = results.find((result) => result.status === "rejected");
1635
+ if (rejected)
1636
+ throw fatal ?? rejected.reason;
1637
+ if (cleanupConflict)
1638
+ await pauseDependency(session, undefined, new AppError("external_work_cleanup_pending", "Accepted external Work still has an unretired workspace writer", 1), "workspace_reuse");
1639
+ state.last_continuation = null;
1640
+ saveState(context, row, state);
1641
+ continue;
1642
+ }
1643
+ if (state.last_continuation === fanout.fingerprint) {
1644
+ // Re-read committed evidence before pausing; a settlement can race the
1645
+ // graph observation. No new prompt or invocation is issued to reconcile.
1646
+ const latest = executionContext.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
1647
+ const latestStage = latest && [...(JSON.parse(latest.index_json).stage_runs ?? [])].reverse().find((stage) => stage.stage === next.stage);
1648
+ if (latest?.status === "done" || latestStage?.status === "done")
1649
+ continue;
1650
+ if (fanout.authority) {
1651
+ const current = currentStageFinishAuthority(executionContext, next.stage);
1652
+ if (current?.status === "issued" && current.command) {
1653
+ const checkpoint = controllerSemanticCheckpoint(executionContext, { stage: next.stage, phase: "stage_finish", command: current.command, invocationId: current.id });
1654
+ if (JSON.stringify(checkpoint) !== JSON.stringify(fanout.checkpoint))
1655
+ continue;
1656
+ }
1657
+ }
1658
+ let rejected;
1659
+ try {
1660
+ assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1661
+ }
1662
+ catch (error) {
1663
+ if (!(error instanceof AppError))
1664
+ throw error;
1665
+ rejected = error;
1666
+ }
1667
+ await pauseDependency(session, undefined, new AppError(rejected?.code ?? "fanout_stage_nonprogressing", rejected?.message ?? "The coordinator repeated an unchanged continuation", 1, { ...rejected?.details, stage: next.stage, attempt: stageAttempt(executionContext, row.project_id, row.run_id, next.stage), action: fanout.action,
1668
+ checkpoint: JSON.parse(fanout.fingerprint), ...(fanout.authority ? { authority: fanout.authority } : {}), reconciliation_required: true }), "productive_continuation");
1669
+ }
1670
+ retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, action: fanout.action, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
1671
+ saveState(context, row, state);
1672
+ await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
1673
+ }
1674
+ }
1675
+ catch (error) {
1676
+ if (technicalPauseRequested || error instanceof AppError && error.code === "controller_technical_dependency_paused")
1677
+ return;
1678
+ if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
1679
+ throw error;
1680
+ let cause = fatal ?? error;
1681
+ if (!fatal && terminalFinalization) {
1682
+ const record = errorRecord(error);
1683
+ const provenance = { phase: "terminal_finalization", project_root: manifest.request.project_root, run_id: row.run_id, controller_id: row.controller_id };
1684
+ if (error instanceof AppError)
1685
+ Object.assign(error.details, provenance);
1686
+ else
1687
+ cause = new AppError(String(record.code), String(record.message), 1, { ...record.details, ...provenance, cause: record });
1688
+ }
1689
+ await stopAfterFailure(cause);
1690
+ throw fatal ?? cause;
1691
+ }
1692
+ finally {
1693
+ clearInterval(monitor);
1694
+ const drainedObservations = [...childObservers.values()].map(retained => retained.close());
1695
+ observationCancellation.abort();
1696
+ await Promise.allSettled(drainedObservations);
1697
+ if (stopping)
1698
+ await stopping;
1699
+ observer.db.close?.();
1700
+ }
1701
+ }
1702
+ function readContexts(file, expected) {
1703
+ const { bytes } = prepareFileBytes(file, "Managed context package");
1704
+ if (digestBytes(bytes) !== expected)
1705
+ throw new AppError("context_checksum_mismatch", "Managed context package SHA-256 differs", 2);
1706
+ const value = prepareJsonText(bytes.toString("utf8"), { file, label: "Managed context package" }).value;
1707
+ if (!value || value.schema_id !== "dd-flow/managed-context@1" || !value.stages || typeof value.stages !== "object" || Array.isArray(value.stages) || Object.keys(value).some(key => !["schema_id", "stages"].includes(key)))
1708
+ throw new AppError("validation", "Expected dd-flow/managed-context@1 with stage file/hash references", 2);
1709
+ const result = {};
1710
+ for (const [stage, source] of Object.entries(value.stages)) {
1711
+ if (!/^[a-z][a-z-]*$/.test(stage) || !source || typeof source !== "object" || Object.keys(source).some(key => !["file", "sha256"].includes(key)) || typeof source.file !== "string" || !path.isAbsolute(source.file) || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
1712
+ throw new AppError("validation", "Invalid managed stage context reference", 2);
1713
+ const { bytes: content } = loadExternalStageContext({ file: source.file, expectedSha256: source.sha256, stage });
1714
+ result[stage] = { bytes: content, sha256: source.sha256 };
1715
+ }
1716
+ return result;
1717
+ }