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

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 +959 -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 +230 -106
  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,1109 @@
1
+ import { setFlowRunRecoveryPaused } from "./runs.js";
2
+ import crypto from "node:crypto";
3
+ import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
4
+ import { readTechnicalNotes, recordTechnicalNote } from "./technical-notes.js";
5
+ import fs from "node:fs";
6
+ import { ensureRunControlStorage } from "../storage/database.js";
7
+ import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
8
+ import { AppError } from "../shared/errors.js";
9
+ import { requireProjectByRoot } from "./projects.js";
10
+ import { recoveryGuard } from "./run-recovery.js";
11
+ import path from "node:path";
12
+ import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
13
+ import { runtimeHarness } from "./execution-policy.js";
14
+ import { harnessKey } from "./harness-config.js";
15
+ import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
16
+ import { reconcileUnfinishedChecks, terminalCheckSettled } from "./code-checks.js";
17
+ import { controlStatusReceipt } from "./run-control-receipt.js";
18
+ import { assertNativeCancellationReceipt, cancelControlledNativeSession, inspectControlledNativeSession } from "./native-session-control.js";
19
+ import { terminalControllerFinalization } from "./run-controller-state.js";
20
+ import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
21
+ import { requireManagedDaemonBinding, managedDaemonExecutionAbsent } from "./managed-daemon-binding.js";
22
+ import { controllerObligationAccepted, technicalDependencyControl } from "./controller-result-authority.js";
23
+ import { requireController } from "./run-controller-state.js";
24
+ /** Durable projection only: status never probes a provider or acquires an owner. */
25
+ export function runControlStatus(context, input) {
26
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
27
+ // One read snapshot prevents a concurrent stop/resume from mixing generations
28
+ // across the request, arbiter and worker projections. Savepoints also nest.
29
+ return context.db.readSnapshot(() => ({ ...readControlStatus(context, input, project), technical_notes: readTechnicalNotes(context, project.id, input.runId) }));
30
+ }
31
+ function readControlStatus(context, input, project) {
32
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
33
+ if (!run)
34
+ throw new AppError("not_found", "RUN is not registered", 1);
35
+ const scope = { project_id: project.id, run_id: input.runId };
36
+ const empty = controlStatusReceipt({ ok: true, scope, run_status: run.status, finalization: terminalControllerFinalization(context, project, input.runId, run.status), control: null, operations: [], worker: null, resume_operation: null }, input);
37
+ if (input.controlId === null)
38
+ return empty;
39
+ if (!context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'"))
40
+ return empty;
41
+ const row = context.db.get("SELECT c.*, g.generation, g.status AS guard_status, g.settlement_json, g.capture_path FROM run_controls c JOIN run_recovery_guards g ON g.recovery_id = c.recovery_id WHERE c.project_id = ? AND c.run_id = ? AND (? IS NULL OR c.control_id = ?) ORDER BY g.generation DESC LIMIT 1", [project.id, input.runId, input.controlId ?? null, input.controlId ?? null]);
42
+ if (!row)
43
+ return empty;
44
+ const current = recoveryGuard(context, project.id, input.runId);
45
+ const operations = context.db.all("SELECT operation_id, target_json, status, receipt_json, error_json, updated_at FROM run_control_operations WHERE control_id = ? ORDER BY created_at, operation_id", [row.control_id]);
46
+ const worker = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_control_workers'")
47
+ ? context.db.get("SELECT status, process_id, owner_pid, snapshot_json, error_json, updated_at FROM run_control_workers WHERE control_id = ?", [row.control_id]) : undefined;
48
+ const resume = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controller_operations'")
49
+ ? context.db.get("SELECT o.operation_id, o.controller_id, json_extract(o.input_json, '$.request_id') AS request_id, o.status, o.receipt_json, o.error_json, o.updated_at FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id WHERE c.project_id = ? AND c.run_id = ? AND json_extract(o.input_json, '$.action') = 'control_resume' AND json_extract(o.input_json, '$.control_id') = ? ORDER BY o.created_at DESC, o.rowid DESC LIMIT 1", [project.id, input.runId, row.control_id]) : undefined;
50
+ return controlStatusReceipt({
51
+ ...empty,
52
+ control: {
53
+ control_id: row.control_id, request_id: row.request_id, recovery_id: row.recovery_id,
54
+ generation: row.generation, requested_mode: row.action, status: row.status,
55
+ current: current?.recovery_id === row.recovery_id,
56
+ admission: row.guard_status,
57
+ request: JSON.parse(row.request_json),
58
+ // A completed interrupt operation is not evidence of whole-tree quiescence.
59
+ settlement: row.settlement_json ? JSON.parse(row.settlement_json) : null,
60
+ capture_path: row.capture_path,
61
+ created_at: row.created_at, updated_at: row.updated_at
62
+ },
63
+ operations: operations.map(operation => ({ operation_id: operation.operation_id, target: JSON.parse(operation.target_json), status: operation.status, receipt: operation.receipt_json ? JSON.parse(operation.receipt_json) : null, error: operation.error_json ? JSON.parse(operation.error_json) : null, updated_at: operation.updated_at })),
64
+ worker: worker ? { status: worker.status, process_id: worker.process_id, owner_pid: worker.owner_pid, snapshot: worker.snapshot_json ? JSON.parse(worker.snapshot_json) : null, error: worker.error_json ? JSON.parse(worker.error_json) : null, updated_at: worker.updated_at } : null,
65
+ resume_operation: resume ? { operation_id: resume.operation_id, controller_id: resume.controller_id, request_id: resume.request_id, status: resume.status, receipt: resume.receipt_json ? JSON.parse(resume.receipt_json) : null, error: resume.error_json ? JSON.parse(resume.error_json) : null, updated_at: resume.updated_at } : null
66
+ }, input);
67
+ }
68
+ /** Interrupt retained managed roots independently; a reply does not prove tree settlement. */
69
+ export async function interruptControlledSessions(context, input) {
70
+ return await reconcileOwnedSessions(context, input, true);
71
+ }
72
+ export async function observeControlledSessions(context, input) {
73
+ return await reconcileOwnedSessions(context, input, false);
74
+ }
75
+ /** The capturing owner is the sole live controller exempted from settlement.
76
+ * Its caller retains the verified inventory until immutable publication. */
77
+ export async function observeControllerBoundary(context, input) {
78
+ return await reconcileOwnedSessions(context, input, false, input);
79
+ }
80
+ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
81
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
82
+ const control = input.controlId ? context.db.get("SELECT * FROM run_controls WHERE project_id = ? AND run_id = ? AND control_id = ?", [project.id, input.runId, input.controlId]) : undefined;
83
+ const request = control && JSON.parse(control.request_json);
84
+ const technicalPause = Boolean(control && technicalDependencyControl(context, project.id, input.runId, control.control_id));
85
+ const pauseEscalated = control?.action === "pause" && request?.escalate === "interrupt" && Number.isSafeInteger(request.grace_ms) && Date.now() >= Date.parse(control.created_at) + request.grace_ms;
86
+ if (boundary ? interrupt || control : !control || interrupt && !(control.action === "stop" || pauseEscalated) || control.status !== "requested")
87
+ throw new AppError("run_control_invalid_state", "Interrupt requires the current stop intent or its explicitly elapsed pause escalation", 1);
88
+ const observationId = boundary?.operationId ?? control.control_id;
89
+ const assertCurrent = () => {
90
+ context.assertControlOwner?.();
91
+ if (boundary) {
92
+ boundary.assertCurrent();
93
+ return;
94
+ }
95
+ const guard = recoveryGuard(context, project.id, input.runId);
96
+ if (guard?.recovery_id !== control.recovery_id || guard.status !== "draining")
97
+ throw new AppError("run_control_superseded", "Stop intent no longer owns this RUN", 1);
98
+ };
99
+ assertCurrent();
100
+ const databaseVersion = () => runSettlementVersion(context, project.id, input.runId, boundary?.operationId);
101
+ const initialDatabaseVersion = databaseVersion();
102
+ const readBindings = () => context.db.all("SELECT project_id, session_id, provider_session_id, harness, parent_session_id, provider_parent_session_id, run_id, status, stop_reason, metadata_json FROM sessions");
103
+ const foreign = (binding) => binding.project_id !== project.id || Boolean(binding.run_id && binding.run_id !== input.runId);
104
+ let bindings = readBindings();
105
+ const underRoot = (binding, root, harness) => {
106
+ const seen = new Set();
107
+ let current = binding;
108
+ while (current && !seen.has(current.session_id)) {
109
+ if (harness && current.harness !== harness)
110
+ return false;
111
+ if (current.provider_session_id === root)
112
+ return true;
113
+ seen.add(current.session_id);
114
+ const parent = current.parent_session_id, nativeParent = current.provider_parent_session_id, currentHarness = current.harness, currentProject = current.project_id;
115
+ if (nativeParent === root)
116
+ return true;
117
+ current = bindings.find(candidate => candidate.project_id === currentProject && candidate.harness === currentHarness && (parent ? candidate.session_id === parent : nativeParent && candidate.provider_session_id === nativeParent));
118
+ }
119
+ return false;
120
+ };
121
+ const controllers = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")
122
+ ? context.db.all("SELECT controller_id, state_json, manifest_json, process_id, process_lease_token FROM run_controllers WHERE project_id = ? AND run_id = ?", [project.id, input.runId]) : [];
123
+ const targets = new Map();
124
+ const unresolved = [];
125
+ const conflicts = new Set();
126
+ const directoryAdapters = new Map();
127
+ const directoryHarnesses = new Map();
128
+ const resourceHomes = new Set([resourceHome(context)]);
129
+ const addTarget = (session, executable, harness = null) => {
130
+ if (session.owner.project_id !== project.id || session.owner.run_id !== input.runId || session.owner.state_dir !== session.state_dir || !path.isAbsolute(session.state_dir))
131
+ throw new AppError("controller_owner_mismatch", "Stored Session does not belong to the controlled RUN", 1);
132
+ if (!executable || !path.isAbsolute(executable))
133
+ throw new AppError("harness_runtime_missing", "Controlled Session has no frozen adapter executable", 1);
134
+ const key = JSON.stringify([session.state_dir, session.id]);
135
+ if (conflicts.has(session.state_dir) || directoryAdapters.has(session.state_dir) && directoryAdapters.get(session.state_dir) !== executable || harness && directoryHarnesses.has(session.state_dir) && directoryHarnesses.get(session.state_dir) !== harness) {
136
+ for (const [targetKey, target] of targets)
137
+ if (target.session.state_dir === session.state_dir)
138
+ targets.delete(targetKey);
139
+ conflicts.add(session.state_dir);
140
+ throw new AppError("controller_target_conflict", "Stored Session has conflicting adapter identities", 1);
141
+ }
142
+ directoryAdapters.set(session.state_dir, executable);
143
+ if (harness)
144
+ directoryHarnesses.set(session.state_dir, harness);
145
+ if (session.owner.resource_home && path.isAbsolute(session.owner.resource_home))
146
+ resourceHomes.add(session.owner.resource_home);
147
+ targets.set(key, { session, executable, harness: harness ?? directoryHarnesses.get(session.state_dir) ?? null });
148
+ };
149
+ for (const row of controllers) {
150
+ let state, manifest;
151
+ try {
152
+ state = JSON.parse(row.state_json);
153
+ manifest = JSON.parse(row.manifest_json);
154
+ if (!Array.isArray(state?.sessions) || !manifest?.adapters || typeof manifest.adapters !== "object")
155
+ throw new Error("Invalid retained inventory");
156
+ if (manifest.resource_home !== undefined) {
157
+ if (!path.isAbsolute(manifest.resource_home))
158
+ throw new Error("Invalid controller resource home");
159
+ resourceHomes.add(manifest.resource_home);
160
+ }
161
+ }
162
+ catch {
163
+ unresolved.push({ controller_id: row.controller_id, status: "pending", reason: "controller_inventory_invalid" });
164
+ continue;
165
+ }
166
+ for (const session of state.sessions) {
167
+ try {
168
+ const executable = manifest.adapters[runtimeHarness(session.profile)]?.executable;
169
+ if (!session.id) {
170
+ const create = context.db.get("SELECT operation_id FROM run_controller_operations WHERE controller_id = ? AND json_extract(input_json, '$.state_dir') = ? AND json_extract(input_json, '$.args[0]') = 'session' AND json_extract(input_json, '$.args[1]') = 'create' ORDER BY created_at DESC LIMIT 1", [row.controller_id, session.state_dir]);
171
+ if (create)
172
+ session.create_operation_id = create.operation_id;
173
+ }
174
+ addTarget(session, executable, runtimeHarness(session.profile));
175
+ }
176
+ catch (error) {
177
+ unresolved.push({ controller_id: row.controller_id, session_id: typeof session?.id === "string" ? session.id : null, status: "pending", reason: error instanceof AppError ? error.code : "controller_inventory_invalid" });
178
+ }
179
+ }
180
+ }
181
+ const launches = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'work_launches'")
182
+ ? context.db.all("SELECT l.* FROM work_launches l JOIN works w ON w.work_id = l.work_id WHERE w.project_id = ? AND w.run_id = ?", [project.id, input.runId]) : [];
183
+ for (const launch of launches) {
184
+ try {
185
+ const profile = JSON.parse(launch.profile_json);
186
+ const binding = profile.runtime_adapter;
187
+ if (!binding)
188
+ throw new AppError("harness_runtime_missing", "External launch has no frozen adapter binding", 1);
189
+ if (binding.owner.work_id !== launch.work_id || binding.owner.operation_id !== launch.operation_id)
190
+ throw new AppError("controller_owner_mismatch", "External launch owner does not match its retained operation", 1);
191
+ addTarget({ id: launch.provider_session_id ?? "", state_dir: launch.state_dir, create_operation_id: `${launch.operation_id}:session.create`, owner: binding.owner }, binding.executable, runtimeHarness(profile.worker));
192
+ }
193
+ catch (error) {
194
+ unresolved.push({ work_id: launch.work_id, operation_id: launch.operation_id, status: "pending", reason: error instanceof AppError ? error.code : "external_inventory_invalid" });
195
+ }
196
+ }
197
+ const merges = context.db.all("SELECT merge_request_id, executor_work_id, dispatch_owner, dispatch_lease_token, adapter_receipt_json FROM merge_requests WHERE project_id = ? AND run_id = ? AND (dispatch_owner IS NOT NULL OR adapter_receipt_json IS NOT NULL)", [project.id, input.runId]);
198
+ for (const merge of merges) {
199
+ try {
200
+ if (!merge.adapter_receipt_json)
201
+ throw new AppError("merge_inventory_missing", "Claimed MERGE has no retained launch receipt", 1);
202
+ const receipt = JSON.parse(merge.adapter_receipt_json);
203
+ if (receipt.controller_id) {
204
+ const controller = controllers.find(row => row.controller_id === receipt.controller_id && row.controller_id === merge.dispatch_owner);
205
+ const state = controller && JSON.parse(controller.state_json);
206
+ if (!state || !state.sessions.some(session => session.id === receipt.session_id && session.state_dir === receipt.state_dir))
207
+ throw new AppError("controller_owner_mismatch", "MERGE receipt has no matching managed Session", 1);
208
+ // The controller inventory already validates and controls this physical root.
209
+ continue;
210
+ }
211
+ if (!receipt.owner)
212
+ throw new AppError("harness_runtime_missing", "MERGE launch has no frozen owner binding", 1);
213
+ if (receipt.owner.role !== "merge" || receipt.owner.work_id !== merge.executor_work_id || !merge.dispatch_lease_token || receipt.owner.operation_id !== `${merge.merge_request_id}:${merge.dispatch_lease_token}`)
214
+ throw new AppError("controller_owner_mismatch", "MERGE owner does not match its retained dispatch", 1);
215
+ addTarget({ id: receipt.session_id ?? "", state_dir: receipt.state_dir, create_operation_id: `${receipt.owner.operation_id}:session.create`, owner: receipt.owner }, receipt.executable, runtimeHarness(receipt.profile));
216
+ }
217
+ catch (error) {
218
+ unresolved.push({ merge_request_id: merge.merge_request_id, status: "pending", reason: error instanceof AppError ? error.code : "merge_inventory_invalid" });
219
+ }
220
+ }
221
+ const processTargets = [];
222
+ const registeredProcesses = [];
223
+ const daemonInventories = new Map();
224
+ const unsafeDaemons = new Set();
225
+ for (const home of resourceHomes) {
226
+ let processes;
227
+ try {
228
+ const all = managedProcessStatus({ ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: home } });
229
+ const homeMatches = (record) => { const ownHome = managedProcessRuntimeHome(record, context); return ownHome === null || ownHome === context.ddFlowHome; };
230
+ for (const record of all.filter(record => record.project_id !== project.id || record.run_id !== input.runId || !homeMatches(record))) {
231
+ try {
232
+ const directory = JSON.parse(record.metadata_json).state_dir;
233
+ if (typeof directory === "string")
234
+ unsafeDaemons.add(canonicalPath(directory, false));
235
+ }
236
+ catch { /* No directory attribution is available. */ }
237
+ }
238
+ processes = all.filter(record => record.project_id === project.id && record.run_id === input.runId && homeMatches(record));
239
+ }
240
+ catch {
241
+ unresolved.push({ resource_home: home, status: "pending", reason: "resource_inventory_unavailable" });
242
+ continue;
243
+ }
244
+ for (const record of processes) {
245
+ registeredProcesses.push({ home, record });
246
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome) {
247
+ unresolved.push({ process_id: record.id, status: "pending", reason: "managed_process_runtime_scope_unknown" });
248
+ continue;
249
+ }
250
+ if (["run-controller", "run-control"].includes(record.kind))
251
+ continue;
252
+ try {
253
+ const metadata = JSON.parse(record.metadata_json);
254
+ if (!metadata.state_dir) {
255
+ // Retain explicit physical evidence for exited resources too; omitting
256
+ // them would make the sealed inventory impossible to account for.
257
+ processTargets.push({ home, record });
258
+ continue;
259
+ }
260
+ if (!path.isAbsolute(metadata.state_dir))
261
+ throw new AppError("controller_owner_mismatch", "Registered state directory is not absolute", 1);
262
+ const executable = metadata.adapter_executable ?? directoryAdapters.get(metadata.state_dir);
263
+ if (!executable)
264
+ throw new AppError("harness_runtime_missing", "Registered runtime has no frozen adapter executable", 1);
265
+ const state = JSON.parse(fs.readFileSync(path.join(metadata.state_dir, "daemon.json"), "utf8"));
266
+ const owner = state.config?.runtime_owner;
267
+ if (!owner || owner.owner_id !== record.owner_id || owner.role !== metadata.role || owner.project_id !== project.id || owner.run_id !== input.runId || owner.state_dir !== metadata.state_dir || (owner.work_id ?? null) !== record.work_id || !owner.operation_id || !record.operation_id?.startsWith(`${owner.operation_id}:`) || owner.dd_flow_home !== context.ddFlowHome || owner.resource_home !== home || owner.adapter_executable !== undefined && owner.adapter_executable !== executable)
268
+ throw new AppError("controller_owner_mismatch", "Retained daemon owner differs from its registered runtime", 1);
269
+ if (!Array.isArray(state.sessions))
270
+ throw new AppError("daemon_inventory_invalid", "Retained daemon has no Session inventory", 1);
271
+ const inventory = daemonInventories.get(metadata.state_dir);
272
+ if (inventory)
273
+ inventory.records.push(record);
274
+ else
275
+ daemonInventories.set(metadata.state_dir, { state, records: [record] });
276
+ // All bindings are checked; the target map deduplicates shared-daemon Sessions.
277
+ for (const item of state.sessions) {
278
+ const id = typeof item === "string" && item.trim() ? item : adapterSessionId(item);
279
+ if (!id)
280
+ throw new AppError("daemon_inventory_invalid", "Retained daemon Session identity is invalid", 1);
281
+ const alias = typeof state.schema_id === "string" ? /^dd-([a-z]+)\/daemon-state@/.exec(state.schema_id)?.[1] : null;
282
+ addTarget({ id, state_dir: metadata.state_dir, owner }, executable, alias ? harnessKey(alias) : null);
283
+ }
284
+ }
285
+ catch (error) {
286
+ try {
287
+ const directory = JSON.parse(record.metadata_json).state_dir;
288
+ if (typeof directory === "string")
289
+ unsafeDaemons.add(canonicalPath(directory, false));
290
+ }
291
+ catch { /* The invalid record remains unresolved below. */ }
292
+ unresolved.push({ process_id: record.id, status: "pending", reason: error instanceof AppError ? error.code : "daemon_inventory_unavailable" });
293
+ }
294
+ }
295
+ }
296
+ const closedDaemons = new Map();
297
+ const nativeVersions = new Map();
298
+ for (const [directory, { state, records }] of daemonInventories) {
299
+ let failedStart = null;
300
+ // Snapshot execution safety is not a claim of clean native shutdown.
301
+ // A technical pause can seal after its real writer is gone even when the
302
+ // third-party session/card never acknowledged completion.
303
+ const physicalContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: registeredProcesses.find(item => records.includes(item.record))?.home ?? resourceHome(context) } };
304
+ let executionAbsent = false;
305
+ let absenceVersion;
306
+ if ((boundary || technicalPause || control?.action === "stop") && !interrupt && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))) {
307
+ try {
308
+ absenceVersion = nativeInventoryVersion(directory);
309
+ executionAbsent = managedDaemonExecutionAbsent(physicalContext, { stateDir: directory, projectId: project.id, runId: input.runId })
310
+ && absenceVersion === nativeInventoryVersion(directory);
311
+ }
312
+ catch { /* Unknown physical ownership remains on strict reconciliation. */ }
313
+ }
314
+ if (executionAbsent) {
315
+ const leader = records.find(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.lease_token === state.resource_process.lease_token);
316
+ if (leader && (state.daemon_id ?? state.config?.daemonId)) {
317
+ nativeVersions.set(directory, absenceVersion);
318
+ closedDaemons.set(directory, { source: "owned_execution_absent", daemon_id: state.daemon_id ?? state.config?.daemonId,
319
+ session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), execution_settled: true,
320
+ native_cleanup_confirmed: state.shutdown_state === "clean", processes: records.map(record => managedProcessBinding(record, registeredProcesses.find(item => item.record === record).home)) });
321
+ if (state.shutdown_state !== "clean")
322
+ recordTechnicalNote(context, project.id, input.runId, { code: "native_cleanup_pending_after_execution_exit", message: "Owned execution is physically absent; native cleanup acknowledgement remains unavailable.", phase: "capture_observation", generation: recoveryGuard(context, project.id, input.runId)?.generation ?? 0, resource_identity: directory, diagnostic_reference: directory, consequence: "cleanup_pending" });
323
+ continue;
324
+ }
325
+ }
326
+ if (!interrupt) {
327
+ try {
328
+ const nativeOperations = retainedDaemonOperations(directory);
329
+ nativeVersions.set(directory, nativeInventoryVersion(directory, nativeOperations));
330
+ // A failed start with no other native request cannot have dispatched a
331
+ // create: productive dispatch always journals its request first.
332
+ const start = nativeOperations.length === 1 ? nativeOperations[0] : null;
333
+ const daemon = records.filter(record => record.kind.endsWith("-daemon"));
334
+ const provider = records.filter(record => record.kind.endsWith("-provider"));
335
+ const owner = state.config?.runtime_owner;
336
+ const launch = start?.launch;
337
+ const controllerStarts = state.shutdown_state === "starting" && controllers.length
338
+ ? context.db.all("SELECT o.operation_id, o.input_json FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id WHERE c.project_id = ? AND c.run_id = ? AND json_extract(o.input_json, '$.state_dir') = ?", [project.id, input.runId, directory]) : [];
339
+ const starts = controllerStarts.filter(operation => {
340
+ const saved = JSON.parse(operation.input_json);
341
+ return saved.owner === owner?.owner_id && saved.executable === directoryAdapters.get(directory) && saved.args?.[0] === "daemon" && saved.args?.[1] === "start" && saved.args.includes("--state-dir") && saved.args[saved.args.indexOf("--state-dir") + 1] === directory;
342
+ });
343
+ const cleanup = start?.error?.details;
344
+ const proof = cleanup?.startup_child_cleanup;
345
+ let ownedChildGone = false;
346
+ if (proof && !cleanup?.provider_cleanup_failed && !cleanup?.cleanup_failed && provider.length === 0 && !state.provider_process && state.provider_pid == null
347
+ && daemon.length === 1 && daemon[0].pid === null && daemon[0].pid_started_at === null
348
+ && proof.settled === true && proof.physical_exit === "ESRCH" && proof.daemon_id === state.daemon_id
349
+ && proof.process_id === daemon[0].id
350
+ && proof.pid === state.pid && Number.isSafeInteger(proof.pid) && Number(proof.pid) > 0
351
+ && proof.pgid === (process.platform === "win32" ? null : proof.pid)
352
+ && typeof proof.observed_at === "string" && Number.isFinite(Date.parse(proof.observed_at))
353
+ && proof.owner_id === owner?.owner_id && proof.project_id === project.id && proof.run_id === input.runId
354
+ && proof.operation_id === start?.operation_id && proof.operation_id === owner?.operation_id && proof.state_dir === directory) {
355
+ let leaderGone = false;
356
+ try {
357
+ process.kill(Number(proof.pid), 0);
358
+ }
359
+ catch (error) {
360
+ leaderGone = error.code === "ESRCH";
361
+ }
362
+ if (leaderGone) {
363
+ if (process.platform === "win32")
364
+ ownedChildGone = true;
365
+ else {
366
+ try {
367
+ process.kill(-Number(proof.pgid), 0);
368
+ }
369
+ catch (error) {
370
+ ownedChildGone = error.code === "ESRCH";
371
+ }
372
+ }
373
+ }
374
+ }
375
+ if (state.shutdown_state === "starting" && state.sessions?.length === 0 && state.active_tree !== true && !state.active_operation
376
+ && start?.operation === "daemon.start" && start.state === "failed" && start.predecessor_daemon_id === null && start.daemon_id === null
377
+ && typeof state.daemon_id === "string" && controllerStarts.length === 1 && starts.length === 1 && starts[0].operation_id === start.operation_id
378
+ && launch?.runtime_owner === JSON.stringify(owner)
379
+ && daemon.length === 1 && provider.length <= 1 && records.length === daemon.length + provider.length
380
+ && daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token
381
+ && (state.pid == null || state.pid === daemon[0].pid || ownedChildGone)
382
+ && (!state.provider_process && state.provider_pid == null && provider.length === 0 || provider.length === 1 && provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && state.provider_pid === provider[0].pid)
383
+ && records.every(record => (record.pid && record.pid_started_at && !processTreeIsAlive(record) || ownedChildGone && record === daemon[0]) && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at))) {
384
+ failedStart = { source: "failed_start_without_create", daemon_id: state.daemon_id, operation_id: start.operation_id, session_ids: [], processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })), ...(ownedChildGone ? { startup_child_cleanup: proof } : {}) };
385
+ closedDaemons.set(directory, failedStart);
386
+ }
387
+ for (const operation of nativeOperations)
388
+ if (!["completed", "failed", "interrupted"].includes(String(operation.state)))
389
+ unresolved.push({ state_dir: directory, operation_id: operation.operation_id, status: "pending", reason: "native_operation_unsettled" });
390
+ if (state.shutdown_state !== "clean" && !failedStart && !records.some(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.pid_started_at && record.lease_token === state.resource_process.lease_token && JSON.parse(record.metadata_json).dispatch_barrier === "run-generation@1"))
391
+ unresolved.push({ state_dir: directory, status: "pending", reason: "native_dispatch_barrier_unverified" });
392
+ }
393
+ catch {
394
+ unresolved.push({ state_dir: directory, status: "pending", reason: "daemon_operation_inventory_invalid" });
395
+ }
396
+ }
397
+ if (closedDaemons.get(directory)?.source === "observed_owned_retirement")
398
+ continue;
399
+ if (state.shutdown_state !== "clean") {
400
+ if (state.shutdown_state === "unclean")
401
+ unresolved.push({ state_dir: directory, status: "pending", reason: "native_shutdown_unclean" });
402
+ if (!state.sessions.length && !failedStart)
403
+ unresolved.push({ state_dir: directory, status: "pending", reason: "daemon_sessions_unknown" });
404
+ continue;
405
+ }
406
+ try {
407
+ const daemonId = state.daemon_id ?? state.config?.daemonId;
408
+ if (!daemonId || state.active_tree === true || state.active_operation || !records.some(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.lease_token === state.resource_process.lease_token))
409
+ throw new AppError("shutdown_identity_unknown", "Closed daemon has no exact retained physical owner", 1);
410
+ if (records.some(record => !record.pid || !record.pid_started_at || processTreeIsAlive(record)))
411
+ throw new AppError("shutdown_process_unsettled", "Closed daemon still has an unconfirmed or live owned process", 1);
412
+ const receipts = retainedDaemonOperations(directory);
413
+ // A whole-RUN stop is also the recovery fence for a create whose reply
414
+ // was lost after native allocation. Keep that one outstanding operation
415
+ // observable until terminalization below; unrelated unfinished work is
416
+ // still a shutdown proof failure.
417
+ const unfinished = receipts.filter(receipt => !["completed", "failed", "interrupted"].includes(String(receipt.state)));
418
+ if (unfinished.some(receipt => receipt.daemon_id !== daemonId || String(receipt.operation) !== "session.create" || receipt.session_id !== null))
419
+ throw new AppError("shutdown_operation_unsettled", "Closed daemon retains an unfinished non-create operation", 1);
420
+ const stopped = receipts.find(receipt => receipt.daemon_id === daemonId && receipt.operation === "daemon.stop" && receipt.state === "completed" && receipt.result?.stopped === true && nativeShutdownConfirmed(receipt.result, { daemonId }));
421
+ if (!stopped)
422
+ throw new AppError("shutdown_receipt_required", "Closed daemon has no clean native shutdown receipt for its current identity", 1);
423
+ closedDaemons.set(directory, { source: "retained_clean_shutdown", daemon_id: daemonId, operation_id: stopped.operation_id, session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), stopped: true, settled: true, processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })), result: stopped.result });
424
+ }
425
+ catch (error) {
426
+ unresolved.push({ state_dir: directory, status: "pending", reason: error instanceof AppError ? error.code : "shutdown_receipt_invalid" });
427
+ }
428
+ }
429
+ for (const [key, target] of targets) {
430
+ if (target.session.id && bindings.some(binding => foreign(binding) && underRoot(binding, target.session.id, target.harness))) {
431
+ targets.delete(key);
432
+ unresolved.push({ session_id: target.session.id, state_dir: target.session.state_dir, status: "pending", reason: "session_scope_conflict" });
433
+ }
434
+ }
435
+ if (!interrupt)
436
+ for (const controller of controllers) {
437
+ const matches = registeredProcesses.filter(item => item.record.id === controller.process_id && item.record.kind === "run-controller" && item.record.owner_id === controller.controller_id && item.record.lease_token === controller.process_lease_token);
438
+ const record = matches.length === 1 ? matches[0].record : null;
439
+ if (boundary?.controllerId === controller.controller_id && record?.pid === process.pid && record.pid_started_at && processIsAlive(record))
440
+ continue;
441
+ if (!record?.pid || !record.pid_started_at || processTreeIsAlive(record))
442
+ unresolved.push({ controller_id: controller.controller_id, process_id: controller.process_id, status: "pending", reason: record?.pid && record.pid_started_at ? "controller_process_running" : "controller_process_identity_unknown" });
443
+ else
444
+ unresolved.push({ controller_id: controller.controller_id, process_id: record.id, status: "completed", receipt: { source: "controller_exited", resource_home: matches[0].home, pid: record.pid, pid_started_at: record.pid_started_at, registration_sha256: crypto.createHash("sha256").update(JSON.stringify([matches[0].home, record.id, record.owner_id, record.lease_token, record.pid, record.pid_started_at])).digest("hex") } });
445
+ }
446
+ const controllerOperations = !interrupt && controllers.length
447
+ ? context.db.all("SELECT o.operation_id, o.controller_id, o.input_json, o.status FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id WHERE c.project_id = ? AND c.run_id = ? AND o.status IN ('dispatching','outcome_unknown')", [project.id, input.runId]) : [];
448
+ const operationNodes = Promise.all(controllerOperations.map(async (operation) => {
449
+ try {
450
+ const saved = JSON.parse(operation.input_json);
451
+ const args = saved.args;
452
+ if (!Array.isArray(args) || args.some(value => typeof value !== "string") || !saved.state_dir || !saved.executable || saved.owner !== operation.controller_id || conflicts.has(saved.state_dir) || directoryAdapters.get(saved.state_dir) !== saved.executable)
453
+ throw new AppError("controller_operation_binding_unknown", "Unfinished controller operation has no matching frozen runtime binding", 1);
454
+ const method = `${args[0]}.${args[1] === "status" ? "inspect" : args[1]}`;
455
+ if (control?.action === "stop" && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
456
+ && (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method)
457
+ || !args.includes("--state-dir") || args[args.indexOf("--state-dir") + 1] !== saved.state_dir
458
+ || args.includes("--session-id") && !targets.has(JSON.stringify([saved.state_dir, args[args.indexOf("--session-id") + 1]]))))
459
+ throw new AppError("controller_operation_binding_unknown", "Fenced operation differs from its retained runtime scope", 1);
460
+ if ((boundary || technicalPause || control?.action === "stop") && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
461
+ && (control?.action === "stop" || ["daemon.stop", "session.cancel", "session.inspect"].includes(method)
462
+ || saved.obligation && controllerObligationAccepted(context, requireController(context, operation.controller_id), saved.obligation))) {
463
+ // This completes the settlement observation, not the original unknown
464
+ // operation. Its retained row/effect remains unknown and is not replayed.
465
+ return { operation_id: operation.operation_id, status: "completed", receipt: { source: control?.action === "stop" ? "fenced_stop_execution_absent" : "accepted_boundary_execution_absent", native_outcome: "unavailable", original_operation_status: operation.status, semantic_outcome_accepted: false, cleanup_pending: true } };
466
+ }
467
+ if (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method))
468
+ throw new AppError("controller_operation_receipt_required", "Unfinished controller operation needs its original receipt", 1);
469
+ const target = args.includes("--session-id") ? args[args.indexOf("--session-id") + 1] : null;
470
+ if (!args.includes("--state-dir") || args[args.indexOf("--state-dir") + 1] !== saved.state_dir || args.includes("--session-id") && (!target || !targets.has(JSON.stringify([saved.state_dir, target]))))
471
+ throw new AppError("controller_operation_binding_unknown", "Unfinished operation arguments differ from the retained Session", 1);
472
+ const retained = JSON.parse(controllers.find(row => row.controller_id === operation.controller_id).state_json).sessions.find(session => session.state_dir === saved.state_dir && (!target || session.id === target));
473
+ if (!retained)
474
+ throw new AppError("controller_operation_binding_unknown", "Unfinished operation has no retained controller Session", 1);
475
+ // A create without a native daemon state file has no provider-side
476
+ // receipt to inspect. Do not invoke the adapter again merely to discover
477
+ // that fact; retain the original outcome-unknown operation for the
478
+ // explicit controller fence/recovery path.
479
+ if (method === "session.create" && !fs.existsSync(path.join(saved.state_dir, "daemon.json")))
480
+ return { operation_id: operation.operation_id, status: "pending", reason: "create_observation_unavailable" };
481
+ const harness = runtimeHarness(retained.profile);
482
+ const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome };
483
+ delete env.DD_FLOW_CONTROLLER_TOKEN;
484
+ delete env.DD_FLOW_CONTROL_TOKEN;
485
+ assertCurrent();
486
+ if (canonicalNativeOperation(harness, method) === "session.inspect") {
487
+ if (!target)
488
+ throw new AppError("controller_operation_binding_unknown", "Native reobservation has no frozen Session target", 1);
489
+ const physicalContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: retained.owner.resource_home ?? resourceHome(context) } };
490
+ const native = requireManagedDaemonBinding(physicalContext, { stateDir: saved.state_dir, projectId: project.id, runId: input.runId, ownerId: operation.controller_id });
491
+ const binding = managedProcessBinding(native.process, resourceHome(physicalContext));
492
+ const assertNative = () => {
493
+ assertCurrent();
494
+ const current = requireManagedDaemonBinding(physicalContext, { stateDir: saved.state_dir, projectId: project.id, runId: input.runId, ownerId: operation.controller_id });
495
+ if (current.state.daemon_id !== native.state.daemon_id || JSON.stringify(managedProcessBinding(current.process, resourceHome(physicalContext))) !== JSON.stringify(binding) || harnessKey(current.process.kind.replace(/-daemon$/, "")) !== harness || !processIsAlive(current.process) || ["starting", "stopping", "clean", "unclean", "cleanup_failed", "failed"].includes(current.state.shutdown_state ?? ""))
496
+ throw new AppError("daemon_owner_mismatch", "Native reobservation lost its live frozen daemon authority", 1);
497
+ };
498
+ const receipt = await inspectControlledNativeSession({ executable: saved.executable, sessionId: target, stateDir: saved.state_dir, harness, env, operationId: `${observationId}:observe:${operation.operation_id}`, assertCurrent: assertNative });
499
+ assertNative();
500
+ context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = ?, error_json = NULL, updated_at = ? WHERE operation_id = ? AND status IN ('dispatching','outcome_unknown')", [JSON.stringify(receipt), context.now(), operation.operation_id]);
501
+ return { operation_id: operation.operation_id, status: "completed", receipt, source: "native_reobservation" };
502
+ }
503
+ const receipt = await runHarnessAdapter({ executable: saved.executable, args: ["daemon", "operation", "--operation-id", operation.operation_id, "--state-dir", saved.state_dir, "--json"], env, operationId: `${observationId}:observe:${operation.operation_id}`, timeoutMs: 30_000 });
504
+ if (receipt.operation_id !== operation.operation_id || receipt.operation !== method || target && receipt.session_id !== target)
505
+ throw new AppError("controller_operation_mismatch", "Operation observation belongs to another retained dispatch", 1);
506
+ if (!["completed", "failed", "interrupted"].includes(String(receipt.state)))
507
+ return { operation_id: operation.operation_id, status: "pending", reason: "controller_operation_unsettled", receipt };
508
+ if (receipt.state === "completed" && (!receipt.result || typeof receipt.result !== "object" || Array.isArray(receipt.result) || target && method !== "daemon.start" && adapterSessionId(receipt.result) !== target || method === "session.create" && !adapterSessionId(receipt.result)))
509
+ throw new AppError("controller_operation_mismatch", "Terminal operation has no valid result for its retained Session", 1);
510
+ if (receipt.state === "completed" && method === "daemon.start" && (typeof receipt.started_daemon_id !== "string" || !receipt.started_daemon_id || receipt.result.daemon_id !== receipt.started_daemon_id))
511
+ throw new AppError("controller_operation_mismatch", "Startup receipt has no confirmed daemon identity", 1);
512
+ if (receipt.state === "completed" && receipt.result.harness !== undefined && receipt.result.harness !== harness)
513
+ throw new AppError("controller_operation_mismatch", "Terminal operation returned another harness identity", 1);
514
+ if (receipt.state === "failed" && (!receipt.error || typeof receipt.error !== "object" || Array.isArray(receipt.error)))
515
+ throw new AppError("controller_operation_mismatch", "Failed operation has no durable error receipt", 1);
516
+ assertCurrent();
517
+ context.db.run("UPDATE run_controller_operations SET status = ?, receipt_json = ?, error_json = ?, updated_at = ? WHERE operation_id = ? AND status IN ('dispatching','outcome_unknown')", [receipt.state, JSON.stringify(receipt.result ?? receipt), receipt.state === "failed" ? JSON.stringify(receipt.error) : null, context.now(), operation.operation_id]);
518
+ return { operation_id: operation.operation_id, status: "completed", receipt };
519
+ }
520
+ catch (error) {
521
+ return { operation_id: operation.operation_id, status: "pending", reason: error instanceof AppError ? error.code : "controller_operation_observation_failed" };
522
+ }
523
+ }));
524
+ for (const check of context.db.all("SELECT id FROM check_receipts WHERE project_id = ? AND run_id = ? AND status = 'running'", [project.id, input.runId])) {
525
+ let reason = "check_settlement_required";
526
+ if (!interrupt) {
527
+ const matches = registeredProcesses.filter(item => item.record.check_id === check.id);
528
+ // A missing/ambiguous registration is not proof that a writer exited.
529
+ // Reuse normal check reconciliation only after exact physical evidence.
530
+ if (matches.length === 1 && matches[0].record.pid && matches[0].record.pid_started_at && !processTreeIsAlive(matches[0].record)) {
531
+ try {
532
+ assertCurrent();
533
+ const run = context.db.get("SELECT workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
534
+ reconcileUnfinishedChecks({ ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: matches[0].home } }, { projectId: project.id, runId: input.runId, workspaceRoot: run.workspace_root, runHome: run.run_root, receiptId: check.id });
535
+ if (context.db.get("SELECT status FROM check_receipts WHERE project_id = ? AND run_id = ? AND id = ?", [project.id, input.runId, check.id])?.status !== "running")
536
+ continue;
537
+ }
538
+ catch (error) {
539
+ reason = error instanceof AppError ? error.code : "check_reconciliation_failed";
540
+ }
541
+ }
542
+ }
543
+ unresolved.push({ check_id: check.id, status: "pending", reason });
544
+ }
545
+ const uncreatedDirectories = new Map();
546
+ const sessionNodes = Promise.all([...targets.values()].map(async ({ session, executable, harness }) => {
547
+ const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome };
548
+ delete env.DD_FLOW_CONTROLLER_TOKEN;
549
+ delete env.DD_FLOW_CONTROL_TOKEN;
550
+ // An explicit pre-registration rejection proves there is no Session to
551
+ // cancel. A missing id or an arbitrary startup failure does not.
552
+ if (!session.id && !session.create_operation_id && !daemonInventories.has(session.state_dir) && !unsafeDaemons.has(canonicalPath(session.state_dir, false)) && !fs.existsSync(path.join(session.state_dir, "daemon.json"))) {
553
+ const operations = context.db.all("SELECT o.operation_id, o.input_json FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id WHERE c.project_id = ? AND c.run_id = ? AND json_extract(o.input_json, '$.state_dir') = ?", [project.id, input.runId, session.state_dir]);
554
+ if (operations.length === 1)
555
+ try {
556
+ const operation = operations[0];
557
+ const saved = JSON.parse(operation.input_json);
558
+ const journal = retainedDaemonOperations(session.state_dir);
559
+ const receipt = journal.length === 1 ? journal[0] : null;
560
+ const error = receipt?.error;
561
+ if (saved.executable === executable && saved.owner === session.owner.owner_id && saved.args?.[0] === "daemon" && saved.args?.[1] === "start" && receipt?.operation_id === operation.operation_id && receipt.operation === "daemon.start" && receipt.predecessor_daemon_id === null && receipt.state === "failed" && error?.details?.startup_resources === "not_created" && !registeredProcesses.some(({ record }) => JSON.parse(record.metadata_json).state_dir === session.state_dir)) {
562
+ uncreatedDirectories.set(session.state_dir, JSON.stringify(journal));
563
+ return { state_dir: session.state_dir, status: "completed", receipt: { source: "startup_resources_not_created", operation_id: operation.operation_id, error } };
564
+ }
565
+ }
566
+ catch { /* Incomplete evidence retains the normal unknown outcome. */ }
567
+ }
568
+ if (!session.id && session.create_operation_id) {
569
+ try {
570
+ assertCurrent();
571
+ const create = await runHarnessAdapter({ executable, args: ["daemon", "operation", "--operation-id", session.create_operation_id, "--state-dir", session.state_dir, "--json"], env, operationId: `${observationId}:observe:${session.create_operation_id}`, timeoutMs: 30_000 });
572
+ if (create.operation_id !== session.create_operation_id || create.operation !== "session.create")
573
+ throw new AppError("controller_operation_mismatch", "Create observation belongs to another operation", 1);
574
+ if (create.state === "completed")
575
+ session.id = adapterSessionId(create.result) ?? "";
576
+ }
577
+ catch (error) {
578
+ return { state_dir: session.state_dir, status: "pending", reason: error instanceof AppError ? error.code : "create_observation_failed" };
579
+ }
580
+ }
581
+ const closed = closedDaemons.get(session.state_dir);
582
+ if (!session.id)
583
+ return closed && !session.create_operation_id && Array.isArray(closed.session_ids) && closed.session_ids.length === 0
584
+ ? { state_dir: session.state_dir, status: "completed", receipt: closed }
585
+ : { state_dir: session.state_dir, create_operation_id: session.create_operation_id ?? null, status: "pending", reason: "provider_identity_unknown" };
586
+ bindings = readBindings();
587
+ if (bindings.some(binding => foreign(binding) && underRoot(binding, session.id, harness)))
588
+ return { session_id: session.id, state_dir: session.state_dir, status: "pending", reason: "session_scope_conflict" };
589
+ if (closed)
590
+ return Array.isArray(closed.session_ids) && closed.session_ids.includes(session.id)
591
+ ? { session_id: session.id, state_dir: session.state_dir, harness, status: "completed", receipt: closed }
592
+ : { session_id: session.id, state_dir: session.state_dir, harness, status: "pending", reason: "shutdown_session_unproven" };
593
+ if (!interrupt) {
594
+ try {
595
+ const receipt = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
596
+ return { session_id: session.id, state_dir: session.state_dir, harness: harness ?? (typeof receipt.harness === "string" ? receipt.harness : null), status: receipt.settled === true ? "completed" : "pending", receipt };
597
+ }
598
+ catch (error) {
599
+ return { session_id: session.id, state_dir: session.state_dir, status: "pending", reason: error instanceof AppError ? error.code : "session_observation_failed" };
600
+ }
601
+ }
602
+ const key = JSON.stringify([session.state_dir, session.id]);
603
+ const operationId = `${observationId}:cancel:${crypto.createHash("sha256").update(key).digest("hex")}`;
604
+ const now = context.now();
605
+ assertCurrent();
606
+ const inserted = context.db.run("INSERT OR IGNORE INTO run_control_operations (operation_id, control_id, target_json, status, created_at, updated_at) VALUES (?, ?, ?, 'dispatching', ?, ?)", [operationId, observationId, JSON.stringify({ session_id: session.id, state_dir: session.state_dir, executable }), now, now]);
607
+ const prior = context.db.get("SELECT status, receipt_json FROM run_control_operations WHERE operation_id = ?", [operationId]);
608
+ let cancellation;
609
+ let cancellationError;
610
+ try {
611
+ if (!inserted.changes && prior.status === "completed") {
612
+ const saved = JSON.parse(prior.receipt_json);
613
+ assertNativeCancellationReceipt(saved, session.id);
614
+ cancellation = saved;
615
+ }
616
+ else {
617
+ const { status, receipt } = await cancelControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, env, operationId, observe: !inserted.changes, assertCurrent });
618
+ const terminalFailure = status === "failed" ? receipt.error && typeof receipt.error === "object" && !Array.isArray(receipt.error) ? receipt.error : { code: "native_cancel_failed" } : null;
619
+ context.db.run("UPDATE run_control_operations SET status = ?, receipt_json = ?, error_json = ?, updated_at = ? WHERE operation_id = ?", [status, JSON.stringify(receipt), terminalFailure ? JSON.stringify(terminalFailure) : null, context.now(), operationId]);
620
+ cancellation = receipt;
621
+ if (terminalFailure)
622
+ cancellationError = { code: typeof terminalFailure.code === "string" ? terminalFailure.code : "native_cancel_failed", receipt };
623
+ }
624
+ }
625
+ catch (error) {
626
+ cancellationError = { code: error instanceof AppError ? error.code : "control_observation_failed", message: String(error) };
627
+ context.db.run("UPDATE run_control_operations SET status = 'outcome_unknown', error_json = ?, updated_at = ? WHERE operation_id = ? AND status != 'completed'", [JSON.stringify(cancellationError), context.now(), operationId]);
628
+ if (cancellationError.code === "controller_operation_mismatch")
629
+ return { operation_id: operationId, session_id: session.id, status: "outcome_unknown", error: cancellationError };
630
+ }
631
+ try {
632
+ const observed = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
633
+ return { operation_id: operationId, session_id: session.id, status: observed.settled === true ? "completed" : "pending", receipt: observed, cancellation, reused: !inserted.changes, ...(cancellationError ? { cancellation_error: cancellationError } : {}) };
634
+ }
635
+ catch (error) {
636
+ return { operation_id: operationId, session_id: session.id, status: "pending", cancellation, cancellation_error: cancellationError, error: { code: error instanceof AppError ? error.code : "control_observation_failed", message: String(error) } };
637
+ }
638
+ }));
639
+ const processResults = Promise.all(processTargets.map(async ({ home, record }) => {
640
+ if (managedLeaseReleased(record))
641
+ return { process_id: record.id, kind: record.kind, status: "completed", receipt: { source: "managed_lease_released", ...managedProcessBinding(record, home), finished_at: record.finished_at } };
642
+ if (record.kind === "workspace-gate")
643
+ return { process_id: record.id, kind: record.kind, status: "pending", reason: "managed_lease_unsettled" };
644
+ if (!record.pid || !record.pid_started_at)
645
+ return { process_id: record.id, kind: record.kind, status: "pending", reason: "managed_process_identity_unknown" };
646
+ // Code checks mark their process terminal only after proving its tree is
647
+ // empty, then publish the exact check receipt. A later PGID reuse cannot
648
+ // turn that already-settled check back into an owned live process.
649
+ const check = record.kind === "check" && record.project_id === project.id && record.run_id === input.runId ? terminalCheckSettled(context, record) : null;
650
+ if (check)
651
+ return { process_id: record.id, kind: record.kind, status: "completed", receipt: { source: "terminal_check_tree_settled", ...managedProcessBinding(record, home), check_id: record.check_id, finished_at: check.finished_at } };
652
+ if (!interrupt && processTreeIsAlive(record))
653
+ return { process_id: record.id, kind: record.kind, status: "pending", reason: "managed_process_running" };
654
+ if (boundary)
655
+ return { process_id: record.id, kind: record.kind, status: "completed", receipt: { source: "owned_process_exited", pid: record.pid, pid_started_at: record.pid_started_at } };
656
+ const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: home } };
657
+ const operationId = `${observationId}:${interrupt ? "interrupt" : "settle"}-process:${crypto.createHash("sha256").update(JSON.stringify([home, record.id, record.pid, record.pid_started_at, record.lease_token])).digest("hex")}`;
658
+ assertCurrent();
659
+ if (!interrupt) {
660
+ const retained = context.db.get("SELECT status, receipt_json FROM run_control_operations WHERE operation_id = ?", [operationId]);
661
+ const receipt = retained?.receipt_json ? JSON.parse(retained.receipt_json) : null;
662
+ // The physical exit was checked above. Do not invalidate the inventory
663
+ // snapshot by rewriting our own already-completed observation each pass.
664
+ if (retained?.status === "completed" && receipt?.process_id === record.id && receipt.settled === true)
665
+ return { operation_id: operationId, process_id: record.id, status: "completed", receipt, reused: true };
666
+ }
667
+ const inserted = context.db.run("INSERT OR IGNORE INTO run_control_operations (operation_id, control_id, target_json, status, created_at, updated_at) VALUES (?, ?, ?, 'dispatching', ?, ?)", [operationId, observationId, JSON.stringify({ resource_home: home, process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at }), context.now(), context.now()]);
668
+ try {
669
+ assertCurrent();
670
+ const current = managedProcessStatus(resourceContext).find(candidate => candidate.id === record.id);
671
+ if (!current || current.lease_token !== record.lease_token || current.pid !== record.pid || current.pid_started_at !== record.pid_started_at)
672
+ throw new AppError("process_lease_lost", "Controlled process identity changed before settlement", 1);
673
+ // The previous owner may have persisted intent without sending its signal.
674
+ // Stop is repeatable for this exact lease/PID/birth; no work is replayed.
675
+ if (interrupt)
676
+ await stopManagedProcess(resourceContext, { id: record.id, leaseToken: record.lease_token, force: JSON.parse(control.request_json).force === true, graceMs: 500, reason: `run_control:${observationId}` });
677
+ else if (processTreeIsAlive(current))
678
+ throw new AppError("process_tree_not_settled", "This owned process tree is still running", 1);
679
+ else if (!finishManagedProcess(resourceContext, { id: record.id, leaseToken: record.lease_token, state: current.state === "failed" ? "failed" : "stopped", reason: `run_control:${observationId}` }))
680
+ throw new AppError("process_lease_lost", "Controlled process ownership changed before settlement", 1);
681
+ const receipt = { process_id: record.id, settled: true };
682
+ context.db.run("UPDATE run_control_operations SET status = 'completed', receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(receipt), context.now(), operationId]);
683
+ return { operation_id: operationId, process_id: record.id, status: "completed", receipt, reused: !inserted.changes };
684
+ }
685
+ catch (error) {
686
+ const failure = { code: error instanceof AppError ? error.code : error.code ?? "process_control_failed", message: String(error) };
687
+ context.db.run("UPDATE run_control_operations SET status = 'outcome_unknown', error_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(failure), context.now(), operationId]);
688
+ return { operation_id: operationId, process_id: record.id, status: "pending", reason: failure.code };
689
+ }
690
+ }));
691
+ const [nodes, processNodes, operations] = await Promise.all([sessionNodes, processResults, operationNodes]);
692
+ // Every whole-RUN stop retires its owned daemon boundary once cancellation
693
+ // has been admitted. `force` controls escalation inside that lifecycle; it
694
+ // must not decide whether the lifecycle exists at all.
695
+ const closeNodes = interrupt && control?.action === "stop" ? await Promise.all([...daemonInventories].map(async ([directory, { state, records }]) => {
696
+ if (closedDaemons.has(directory)) {
697
+ const receipt = closedDaemons.get(directory);
698
+ return { state_dir: directory, status: "completed", operation_id: receipt.operation_id, receipt };
699
+ }
700
+ bindings = readBindings();
701
+ const ids = state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item));
702
+ const harness = directoryHarnesses.get(directory) ?? null;
703
+ if (conflicts.has(directory) || unsafeDaemons.has(canonicalPath(directory, false)) || ids.some(id => !id || bindings.some(binding => foreign(binding) && underRoot(binding, id, harness))))
704
+ return { state_dir: directory, status: "pending", reason: "daemon_scope_conflict" };
705
+ const daemonId = state.daemon_id ?? state.config?.daemonId;
706
+ const leader = records.find(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.pid_started_at && record.lease_token === state.resource_process.lease_token);
707
+ const executable = directoryAdapters.get(directory) ?? (leader && JSON.parse(leader.metadata_json).adapter_executable);
708
+ if (!daemonId || !leader || !executable)
709
+ return { state_dir: directory, status: "pending", reason: "daemon_close_owner_unproven" };
710
+ let operationId = `${observationId}:close-daemon:${crypto.createHash("sha256").update(JSON.stringify([directory, daemonId])).digest("hex")}`;
711
+ const nativeOperations = retainedDaemonOperations(directory);
712
+ const targetList = [...targets.values()];
713
+ const sessionsSettled = ids.every(id => nodes.some((node, index) => targetList[index]?.session.state_dir === directory && targetList[index]?.session.id === id && node.status === "completed"));
714
+ // A confirmed failure may retry the same incarnation's settled tree or
715
+ // remaining idempotent resource finish. Unknown outcomes keep their ID.
716
+ if (sessionsSettled) {
717
+ const remainingFinish = state.shutdown_state === "cleanup_failed" && state.active_tree === false
718
+ && state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.execution === true
719
+ && state.shutdown.required_phases?.includes("daemon_resource")
720
+ && state.shutdown.phases.daemon_resource !== true
721
+ && state.shutdown.required_phases.every(phase => phase === "daemon_resource" || state.shutdown.phases[phase] === true);
722
+ while (nativeOperations.some(operation => operation.operation_id === operationId && operation.daemon_id === daemonId && operation.operation === "daemon.stop"
723
+ && operation.state === "failed" && (operation.error?.code === "tree_not_settled"
724
+ || remainingFinish && operation.error?.code === "process_maintenance_timeout")))
725
+ operationId += ":finalize";
726
+ }
727
+ assertCurrent();
728
+ const inserted = context.db.run("INSERT OR IGNORE INTO run_control_operations (operation_id, control_id, target_json, status, created_at, updated_at) VALUES (?, ?, ?, 'dispatching', ?, ?)", [operationId, observationId, JSON.stringify({ state_dir: directory, daemon_id: daemonId, executable, cancel_tree: true }), context.now(), context.now()]);
729
+ const prior = context.db.get("SELECT status, receipt_json FROM run_control_operations WHERE operation_id = ?", [operationId]);
730
+ try {
731
+ if (!inserted.changes && prior.status === "completed") {
732
+ const receipt = JSON.parse(prior.receipt_json);
733
+ assertDaemonCloseReceipt(receipt);
734
+ return { operation_id: operationId, state_dir: directory, status: "completed", receipt, reused: true };
735
+ }
736
+ if (inserted.changes) {
737
+ const fresh = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
738
+ const owned = registeredProcesses.find(item => item.record === leader);
739
+ const record = managedProcessStatus({ ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: owned.home } }).find(item => item.id === leader.id);
740
+ if (!record || record.lease_token !== leader.lease_token || record.pid !== leader.pid || record.pid_started_at !== leader.pid_started_at || !processIsAlive(record) || (fresh.daemon_id ?? fresh.config?.daemonId) !== daemonId || fresh.resource_process?.id !== leader.id || fresh.resource_process.lease_token !== leader.lease_token || JSON.stringify(fresh.config?.runtime_owner) !== JSON.stringify(state.config?.runtime_owner))
741
+ throw new AppError("daemon_close_owner_changed", "Daemon ownership changed before its forced close", 1);
742
+ }
743
+ assertCurrent();
744
+ const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome };
745
+ delete env.DD_FLOW_CONTROL_TOKEN;
746
+ delete env.DD_FLOW_CONTROLLER_TOKEN;
747
+ const receipt = await runHarnessAdapter({ executable, args: inserted.changes ? ["daemon", "stop", "--cancel-tree", "--state-dir", directory, "--json"] : ["daemon", "operation", "--operation-id", operationId, "--state-dir", directory, "--json"], env, operationId: inserted.changes ? operationId : `${operationId}:observe`, timeoutMs: 30_000 });
748
+ if (!inserted.changes && (receipt.operation_id !== operationId || receipt.operation !== "daemon.stop" || receipt.daemon_id !== daemonId))
749
+ throw new AppError("controller_operation_mismatch", "Daemon close observation belongs to another operation or incarnation", 1);
750
+ if (receipt.state === "failed") {
751
+ const failure = receipt.error && typeof receipt.error === "object" && !Array.isArray(receipt.error) ? receipt.error : { code: "daemon_close_failed" };
752
+ context.db.run("UPDATE run_control_operations SET status = 'failed', receipt_json = ?, error_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(receipt), JSON.stringify(failure), context.now(), operationId]);
753
+ return { operation_id: operationId, state_dir: directory, status: "pending", reason: typeof failure.code === "string" ? failure.code : "daemon_close_failed", receipt };
754
+ }
755
+ const observed = inserted.changes ? receipt : receipt.state === "completed" ? receipt.result : null;
756
+ if (observed)
757
+ assertDaemonCloseReceipt(observed);
758
+ const status = observed ? "completed" : "outcome_unknown";
759
+ context.db.run("UPDATE run_control_operations SET status = ?, receipt_json = ?, error_json = NULL, updated_at = ? WHERE operation_id = ?", [status, JSON.stringify(observed ?? receipt), context.now(), operationId]);
760
+ return { operation_id: operationId, state_dir: directory, status, receipt: observed ?? receipt };
761
+ }
762
+ catch (error) {
763
+ const failure = { code: error instanceof AppError ? error.code : "daemon_close_failed", message: String(error) };
764
+ context.db.run("UPDATE run_control_operations SET status = 'outcome_unknown', error_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(failure), context.now(), operationId]);
765
+ return { operation_id: operationId, state_dir: directory, status: "pending", reason: failure.code };
766
+ }
767
+ })) : [];
768
+ // Once the same control owns a clean whole-tree stop, an older productive
769
+ // request cannot gain a provider reply through this daemon. Seal that fact in
770
+ // the shared native journal before the next observation pass. This prevents a
771
+ // closed daemon from becoming a permanent prerequisite for its own recovery.
772
+ const terminalizedOperations = [];
773
+ if (interrupt && control?.action === "stop")
774
+ for (const node of closeNodes) {
775
+ if (node.status !== "completed")
776
+ continue;
777
+ const directory = node.state_dir;
778
+ const inventory = daemonInventories.get(directory);
779
+ const daemonId = inventory?.state.daemon_id ?? inventory?.state.config?.daemonId;
780
+ const executable = directoryAdapters.get(directory);
781
+ if (!inventory)
782
+ continue;
783
+ const pending = retainedDaemonOperations(directory).filter(operation => operation.daemon_id === daemonId && ["running", "awaiting_provider"].includes(String(operation.state)) && ["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(String(operation.operation)) && typeof operation.operation_id === "string");
784
+ if (!pending.length)
785
+ continue;
786
+ if (!daemonId || !executable || !node.operation_id) {
787
+ terminalizedOperations.push({ state_dir: directory, status: "pending", reason: "operation_terminalization_binding_unknown" });
788
+ continue;
789
+ }
790
+ terminalizedOperations.push(...await Promise.all(pending.map(async (operation) => {
791
+ try {
792
+ const operationId = operation.operation_id, sessionId = typeof operation.session_id === "string" ? operation.session_id : null;
793
+ assertCurrent();
794
+ const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome };
795
+ delete env.DD_FLOW_CONTROL_TOKEN;
796
+ delete env.DD_FLOW_CONTROLLER_TOKEN;
797
+ const interruptArgs = ["daemon", "operation", "interrupt", "--operation-id", operationId, "--control-id", control.control_id, "--stop-operation-id", String(node.operation_id), "--daemon-id", String(daemonId)];
798
+ if (sessionId)
799
+ interruptArgs.push("--session-id", sessionId);
800
+ interruptArgs.push("--state-dir", directory, "--json");
801
+ const receipt = await runHarnessAdapter({ executable, args: interruptArgs, env, operationId: `${observationId}:interrupt-operation:${operationId}`, timeoutMs: 30_000 });
802
+ if (receipt.operation_id !== operationId || receipt.state !== "interrupted" || receipt.daemon_id !== daemonId || receipt.session_id !== sessionId)
803
+ throw new AppError("controller_operation_mismatch", "Interrupted operation receipt belongs to another native request", 1);
804
+ return { operation_id: operationId, state_dir: directory, status: "completed", receipt };
805
+ }
806
+ catch (error) {
807
+ return { operation_id: operation.operation_id, state_dir: directory, status: "pending", reason: error instanceof AppError ? error.code : "operation_terminalization_failed" };
808
+ }
809
+ })));
810
+ }
811
+ let verifyInventory = assertCurrent;
812
+ if (!interrupt) {
813
+ const importVersions = new Map();
814
+ const historicalImport = (binding) => {
815
+ if (binding.status !== "stopped" || binding.stop_reason !== "snapshot_restored"
816
+ || context.db.get("SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running' LIMIT 1", [binding.session_id]))
817
+ return false;
818
+ try {
819
+ const marker = JSON.parse(binding.metadata_json).snapshot_import;
820
+ if (typeof marker?.lineage_file !== "string" || typeof marker.lineage_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(marker.lineage_sha256))
821
+ return false;
822
+ if (!fs.lstatSync(marker.lineage_file).isFile() || path.dirname(fs.realpathSync(marker.lineage_file)) !== path.join(fs.realpathSync(context.ddFlowHome), "snapshot-imports"))
823
+ return false;
824
+ const bytes = fs.readFileSync(marker.lineage_file);
825
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== marker.lineage_sha256)
826
+ return false;
827
+ const lineage = JSON.parse(bytes.toString("utf8"));
828
+ if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.restored_runtime !== context.ddFlowHome || lineage.source_runtime === context.ddFlowHome || lineage.run_id !== input.runId || !Array.isArray(lineage.run_tables?.sessions))
829
+ return false;
830
+ const found = lineage.run_tables.sessions.some((row) => ["project_id", "session_id", "provider_session_id", "harness", "run_id"].every(key => row[key] === binding[key]));
831
+ if (found)
832
+ importVersions.set(marker.lineage_file, marker.lineage_sha256);
833
+ return found;
834
+ }
835
+ catch {
836
+ return false;
837
+ }
838
+ };
839
+ bindings = readBindings();
840
+ for (const target of targets.values())
841
+ if (!daemonInventories.has(target.session.state_dir) && !uncreatedDirectories.has(target.session.state_dir))
842
+ unresolved.push({ state_dir: target.session.state_dir, status: "pending", reason: "runtime_inventory_unverified" });
843
+ for (const node of nodes)
844
+ if ("harness" in node && "session_id" in node && typeof node.session_id === "string" && bindings.some(binding => foreign(binding) && underRoot(binding, node.session_id, node.harness ?? null)))
845
+ unresolved.push({ session_id: node.session_id, status: "pending", reason: "session_scope_conflict" });
846
+ const scoped = new Set(bindings.filter(binding => binding.project_id === project.id && binding.run_id === input.runId).map(binding => binding.session_id));
847
+ for (const link of context.db.all("SELECT ws.id, ws.session_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ?", [project.id, input.runId])) {
848
+ scoped.add(link.session_id);
849
+ if (!bindings.some(binding => binding.project_id === project.id && binding.session_id === link.session_id))
850
+ unresolved.push({ work_session_id: link.id, status: "pending", reason: "work_session_binding_unknown" });
851
+ }
852
+ for (const binding of bindings.filter(item => item.project_id === project.id && scoped.has(item.session_id))) {
853
+ // Imported history is not a live native owner. Keep it in `bindings`
854
+ // above so it can never hide foreign descendants or actual process nodes.
855
+ if (historicalImport(binding))
856
+ continue;
857
+ const covered = nodes.some(node => "harness" in node && node.harness === binding.harness && node.status === "completed" && "session_id" in node && typeof node.session_id === "string" && underRoot(binding, node.session_id, node.harness));
858
+ if (!covered)
859
+ unresolved.push({ session_id: binding.provider_session_id, harness: binding.harness, status: "pending", reason: binding.run_id && binding.run_id !== input.runId ? "session_scope_conflict" : "bound_session_settlement_required" });
860
+ }
861
+ for (const work of context.db.all("SELECT w.work_id FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.status = 'running' AND NOT EXISTS (SELECT 1 FROM work_sessions ws WHERE ws.work_id = w.work_id)", [project.id, input.runId]))
862
+ unresolved.push({ work_id: work.work_id, status: "pending", reason: "running_work_binding_unknown" });
863
+ const processVersion = (items) => JSON.stringify(items.map(({ home, record }) => [home, record.id, record.kind, record.owner_id, record.project_id, record.run_id, record.work_id, record.check_id, record.operation_id, record.pid, record.pid_started_at, record.lease_token, record.state, record.metadata_json]).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))));
864
+ verifyInventory = () => {
865
+ assertCurrent();
866
+ for (const [directory, journal] of uncreatedDirectories)
867
+ if (fs.existsSync(path.join(directory, "daemon.json")) || JSON.stringify(retainedDaemonOperations(directory)) !== journal)
868
+ throw new AppError("owned_inventory_changed", "Rejected startup acquired new runtime evidence", 1);
869
+ const currentProcesses = [...resourceHomes].flatMap(home => managedProcessStatus({ ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: home } }).filter(record => record.project_id === project.id && record.run_id === input.runId && [null, context.ddFlowHome].includes(managedProcessRuntimeHome(record, context))).map(record => ({ home, record })));
870
+ const changed = [
871
+ ...(initialDatabaseVersion !== databaseVersion() ? ["run_bindings"] : []),
872
+ ...(processVersion(registeredProcesses) !== processVersion(currentProcesses) ? ["process_owners"] : []),
873
+ ...[...nativeVersions].filter(([directory, version]) => version !== nativeInventoryVersion(directory)).map(([directory]) => `native:${directory}`),
874
+ ...[...importVersions].filter(([file, version]) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== version).map(([file]) => `import:${file}`)
875
+ ];
876
+ if (changed.length)
877
+ throw new AppError("owned_inventory_changed", "Owned writers changed after their settlement observation", 1, { components: changed });
878
+ };
879
+ try {
880
+ verifyInventory();
881
+ }
882
+ catch (error) {
883
+ unresolved.push({ status: "pending", reason: error instanceof AppError && error.code === "owned_inventory_changed" ? error.code : "owned_inventory_recheck_failed",
884
+ ...(error instanceof AppError && error.code === "owned_inventory_changed" ? { components: error.details.components } : {}) });
885
+ }
886
+ }
887
+ assertCurrent();
888
+ // A clean daemon close plus a sealed interruption terminalizes an in-flight
889
+ // create even though its controller Session row never received an id. Do
890
+ // not leave the pre-terminalization `provider_identity_unknown` shadow node
891
+ // in the final result.
892
+ const terminalized = new Set(terminalizedOperations.filter(node => node.status === "completed").map(node => node.operation_id));
893
+ const reconciledNodes = nodes.map(node => {
894
+ const createId = "create_operation_id" in node && typeof node.create_operation_id === "string" ? node.create_operation_id : null;
895
+ return createId && terminalized.has(createId) ? { ...node, status: "completed", reason: undefined, receipt: terminalizedOperations.find(item => item.operation_id === createId)?.receipt } : node;
896
+ });
897
+ const allNodes = [...reconciledNodes, ...processNodes, ...operations, ...closeNodes, ...terminalizedOperations, ...unresolved];
898
+ const settled = !interrupt && allNodes.every(node => node.status === "completed");
899
+ if (settled)
900
+ boundary?.retainVerification(verifyInventory);
901
+ const processBindings = settled ? registeredProcesses.filter(({ record }) => record.kind !== "run-control" && !(boundary && record.kind === "run-controller" && record.owner_id === boundary.controllerId)).map(({ home, record }) => managedProcessBinding(record, home)) : [];
902
+ const pendingReasons = settled ? [] : [...new Set([...(interrupt ? ["settlement_observation_required"] : []), ...allNodes.filter(node => node.status !== "completed").map(node => "reason" in node && typeof node.reason === "string" ? node.reason : "node_settlement_required")])];
903
+ return { ok: true, ...(boundary ? { operation_id: observationId } : { control_id: observationId }), status: "requested", settled, nodes: allNodes, process_bindings: processBindings, resource_homes: [...resourceHomes].sort(), pending_reasons: pendingReasons };
904
+ }
905
+ export function nativeInventoryVersion(directory, operations = retainedDaemonOperations(directory)) {
906
+ const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
907
+ const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes", "native_execution_revision", "unclaimed_activity", "native_resources_entries", "native_topology_error", "native_retirement_notifications", "terminal_observation_pending", "tool_observation_gaps"];
908
+ // Only these native verbs are read-only. Unknown verbs remain guarded.
909
+ const productive = operations.filter(operation => !isObservationOperation(operation.operation));
910
+ const knownSchema = ["dd-opencode/daemon-state@2", "dd-codex/daemon-state@1", "dd-grok/daemon-state@1", "dd-zcode/daemon-state@1", "dd-agy/daemon-state@1", "dd-droid/daemon-state@1"].includes(String(state.schema_id));
911
+ const inventory = knownSchema ? Object.fromEntries(keys.map(key => [key, state[key]])) : { ...state };
912
+ const canonical = (value) => Array.isArray(value) ? value.map(canonical)
913
+ : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonical(item)])) : value;
914
+ // Sets have no observation order. Unknown schemas/fields remain guarded;
915
+ // only the proven derived OpenCode adapter alias is not ownership authority.
916
+ for (const key of ["sessions", "descendants", "owned_processes"]) {
917
+ const values = inventory[key];
918
+ if (!Array.isArray(values))
919
+ continue;
920
+ const seen = new Set();
921
+ inventory[key] = values.map(value => {
922
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
923
+ if (state.schema_id === "dd-opencode/daemon-state@2")
924
+ throw new AppError("native_inventory_invalid", `Invalid native ${key} record`, 1);
925
+ return value;
926
+ }
927
+ const record = value;
928
+ const identity = record.provider_session_id ?? record.process_id ?? record.pid;
929
+ if (identity === undefined && state.schema_id !== "dd-opencode/daemon-state@2")
930
+ return record;
931
+ if (!(typeof identity === "string" && identity.length > 0 || typeof identity === "number" && Number.isSafeInteger(identity) && identity > 0) || seen.has(String(identity)))
932
+ throw new AppError("native_inventory_invalid", `Invalid or duplicate native ${key} identity`, 1);
933
+ seen.add(String(identity));
934
+ if (state.schema_id === "dd-opencode/daemon-state@2" && key === "sessions") {
935
+ if (record.adapter_session_id !== undefined && record.adapter_session_id !== record.provider_session_id)
936
+ throw new AppError("native_inventory_invalid", "Native Session adapter identity differs", 1);
937
+ const authority = { ...record };
938
+ delete authority.adapter_session_id;
939
+ return authority;
940
+ }
941
+ return record;
942
+ }).map(canonical).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
943
+ }
944
+ const productiveInventory = productive.map(canonical).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
945
+ return crypto.createHash("sha256").update(JSON.stringify(canonical([inventory, productiveInventory]))).digest("hex");
946
+ }
947
+ export function isObservationOperation(operation) {
948
+ return ["session.inspect", "daemon.status"].includes(String(operation));
949
+ }
950
+ /** Compare authority and productive state of this RUN, not writes anywhere
951
+ * in the shared database. Heartbeat and observer timestamps are not authority. */
952
+ export function runSettlementVersion(context, projectId, runId, observerOperationId) {
953
+ return context.db.readSnapshot(() => {
954
+ const exists = (table) => Boolean(context.db.get("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [table]));
955
+ const rows = (table, where, values) => exists(table) ? context.db.all(`SELECT * FROM ${table} WHERE ${where}`, values) : [];
956
+ const scoped = [projectId, runId];
957
+ const sessions = rows("sessions", "1 = 1", []);
958
+ const identity = (row) => JSON.stringify([row.project_id, row.session_id]);
959
+ const selected = new Set(sessions.filter(row => row.project_id === projectId && row.run_id === runId).map(identity));
960
+ // Include newly registered children, even before their RUN id is attached.
961
+ // ponytail: retained Session inventories are small; index ancestry if scans become costly.
962
+ let previous = -1;
963
+ while (previous !== selected.size) {
964
+ previous = selected.size;
965
+ for (const row of sessions)
966
+ if (sessions.some(parent => selected.has(identity(parent)) && parent.harness === row.harness && (parent.project_id === row.project_id && parent.session_id === row.parent_session_id || parent.provider_session_id && parent.provider_session_id === row.provider_parent_session_id)))
967
+ selected.add(identity(row));
968
+ }
969
+ // Match the protocol relationships retained by recovery-snapshot-database.
970
+ const protocolIds = "SELECT subject_id FROM runs WHERE project_id = ? AND id = ? UNION SELECT value FROM merge_requests, json_each(protocol_ids_json) WHERE project_id = ? AND run_id = ? UNION SELECT protocol_id FROM flow_jobs WHERE project_id = ? AND run_id = ?";
971
+ const protocolScope = [projectId, ...scoped, ...scoped, ...scoped];
972
+ const projection = {
973
+ runs: rows("runs", "project_id = ? AND id = ?", scoped),
974
+ project: rows("projects", "id = ?", [projectId]),
975
+ project_config: rows("project_config", "project_id = ?", [projectId]),
976
+ protocols: rows("protocols", `project_id = ? AND id IN (${protocolIds})`, protocolScope),
977
+ plan_bindings: rows("plan_bindings", `project_id = ? AND protocol_id IN (${protocolIds})`, protocolScope),
978
+ plan_progress: rows("plan_progress", `project_id = ? AND protocol_id IN (${protocolIds})`, protocolScope),
979
+ worktrees: rows("worktree_records", `project_id = ? AND protocol_id IN (${protocolIds})`, protocolScope),
980
+ sessions: sessions.filter(row => selected.has(identity(row))),
981
+ works: rows("works", "project_id = ? AND run_id = ?", scoped),
982
+ checks: rows("check_receipts", "project_id = ? AND run_id = ?", scoped),
983
+ merges: rows("merge_requests", "project_id = ? AND run_id = ?", scoped),
984
+ memberships: rows("stage_work_memberships", "project_id = ? AND run_id = ?", scoped),
985
+ repairs: rows("repair_intents", "project_id = ? AND run_id = ?", scoped),
986
+ lifecycle: rows("lifecycle_invocations", "json_extract(scope_json, '$.runId') = ? AND json_extract(scope_json, '$.projectRoot') = (SELECT root FROM projects WHERE id = ?)", [runId, projectId]),
987
+ packets: rows("lifecycle_packet_assignments", "invocation_id IN (SELECT id FROM lifecycle_invocations WHERE json_extract(scope_json, '$.runId') = ? AND json_extract(scope_json, '$.projectRoot') = (SELECT root FROM projects WHERE id = ?))", [runId, projectId]),
988
+ flags: rows("flow_run_flag_mutations", "project_id = ? AND run_id = ?", scoped),
989
+ jobs: rows("flow_jobs", "project_id = ? AND run_id = ?", scoped),
990
+ work_sessions: rows("work_sessions", "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)", scoped),
991
+ work_launches: rows("work_launches", "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)", scoped),
992
+ controls: rows("run_controls", "project_id = ? AND run_id = ?", scoped),
993
+ recovery: rows("run_recovery_guards", "project_id = ? AND run_id = ?", scoped),
994
+ operations: rows("run_controller_operations", "controller_id IN (SELECT controller_id FROM run_controllers WHERE project_id = ? AND run_id = ?)", scoped).filter(row => {
995
+ if (row.operation_id === observerOperationId)
996
+ return false;
997
+ const input = JSON.parse(String(row.input_json));
998
+ const operation = input.args?.slice(0, 2).join(".");
999
+ // CLI session status is an alias for the native session.inspect read.
1000
+ return operation !== "session.status" && !isObservationOperation(operation);
1001
+ }).map(row => ({ operation_id: row.operation_id, controller_id: row.controller_id, generation: row.generation, input_hash: row.input_hash, input_json: row.input_json, status: row.status, receipt_json: row.receipt_json, error_json: row.error_json })),
1002
+ controllers: rows("run_controllers", "project_id = ? AND run_id = ?", scoped).map(row => ({ controller_id: row.controller_id, generation: row.generation, status: row.status, owner_token: row.owner_token, process_id: row.process_id, process_lease_token: row.process_lease_token, state_dir: row.state_dir, manifest_json: row.manifest_json, sessions: JSON.parse(String(row.state_json)).sessions }))
1003
+ };
1004
+ return JSON.stringify(Object.fromEntries(Object.entries(projection).map(([table, records]) => [table, records.map(row => Object.fromEntries(Object.entries(row).filter(([key]) => !["created_at", "updated_at", "heartbeat_at"].includes(key)))).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))])));
1005
+ });
1006
+ }
1007
+ /** Read the bundled runtime's durable operation journal, including lost replies. */
1008
+ export function retainedDaemonOperations(directory) {
1009
+ const root = path.join(directory, "operations");
1010
+ if (!fs.existsSync(root))
1011
+ return [];
1012
+ return fs.readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name)).map(entry => {
1013
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name))
1014
+ throw new AppError("daemon_operation_inventory_invalid", "Native operation journal has an invalid entry", 1);
1015
+ const folder = path.join(root, entry.name);
1016
+ const requested = JSON.parse(fs.readFileSync(path.join(folder, "requested.json"), "utf8"));
1017
+ if (requested.schema_id !== "dd-flow/daemon-operation@1" || typeof requested.operation_id !== "string" || crypto.createHash("sha256").update(requested.operation_id).digest("hex") !== entry.name || typeof requested.operation !== "string")
1018
+ throw new AppError("daemon_operation_inventory_invalid", "Native operation request has a conflicting identity", 1);
1019
+ const outcomePath = ["result.json", "interrupted.json", "observation-lost.json"].map(file => path.join(folder, file)).find(file => fs.existsSync(file));
1020
+ const outcome = outcomePath ? JSON.parse(fs.readFileSync(outcomePath, "utf8")) : { state: "running" };
1021
+ if (!outcome || typeof outcome !== "object" || Array.isArray(outcome) || !["completed", "failed", "interrupted", "running", "awaiting_provider"].includes(String(outcome.state)) || ["operation_id", "operation", "daemon_id", "session_id"].some(key => key in outcome && outcome[key] !== requested[key]))
1022
+ throw new AppError("daemon_operation_inventory_invalid", "Native operation result has an invalid identity or state", 1);
1023
+ if (outcome.state === "completed" && (!outcome.result || typeof outcome.result !== "object" || Array.isArray(outcome.result)) || ["failed", "interrupted"].includes(String(outcome.state)) && (!outcome.error || typeof outcome.error !== "object" || Array.isArray(outcome.error)))
1024
+ throw new AppError("daemon_operation_inventory_invalid", "Native operation omits its terminal evidence", 1);
1025
+ return { ...requested, ...outcome };
1026
+ });
1027
+ }
1028
+ function assertDaemonCloseReceipt(value) {
1029
+ if (!value || typeof value !== "object" || value.stopped !== true)
1030
+ throw new AppError("daemon_close_receipt_invalid", "Daemon close has no stopped acknowledgement", 1);
1031
+ }
1032
+ export function controlLifecycleReply(context, input, result) {
1033
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1034
+ const control = runControlDirective(context, project.id, input.runId);
1035
+ if (!control || !result || typeof result !== "object" || Array.isArray(result))
1036
+ return result;
1037
+ return { ...result, control, next: null, next_command: null, next_action: "operator_control", flow_guidance: null, newly_ready: [] };
1038
+ }
1039
+ /** A lifecycle reply can carry the trusted stop request; it is not a physical ACK. */
1040
+ export function runControlDirective(context, projectId, runId) {
1041
+ if (!context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'"))
1042
+ return null;
1043
+ const current = context.db.get("SELECT c.* FROM run_controls c JOIN run_recovery_guards g ON g.recovery_id = c.recovery_id WHERE c.project_id = ? AND c.run_id = ? AND c.status IN ('requested','settled','resuming') AND g.status IN ('draining','sealed','resuming') AND g.generation = (SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = c.project_id AND run_id = c.run_id)", [projectId, runId]);
1044
+ if (!current)
1045
+ return null;
1046
+ return { control_id: current.control_id, action: current.action, status: current.status, stop_turn: true, instruction: "The operator has fenced this RUN. Preserve the recorded result and end this Turn now. Do not start another Work, check, Stage or provider operation, and do not wait for resume in a sleep loop. Only the runtime may authorize continuation.", acknowledged: false, settled: false };
1047
+ }
1048
+ /** The durable fence is committed before observation, steering or interruption. */
1049
+ export function recordRunControlIntent(context, input, onRecorded) {
1050
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !["pause", "stop"].includes(input.action)
1051
+ || input.graceMs !== undefined && (!Number.isSafeInteger(input.graceMs) || input.graceMs < 0 || input.graceMs > 3_600_000)
1052
+ || input.force !== undefined && typeof input.force !== "boolean"
1053
+ || input.escalate !== undefined && input.escalate !== "interrupt"
1054
+ || input.action === "pause" && input.force !== undefined
1055
+ || input.action === "stop" && (input.graceMs !== undefined || input.escalate !== undefined))
1056
+ throw new AppError("usage", "Invalid RUN control intent", 2);
1057
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1058
+ const request = { project_root: project.root, run_id: input.runId, action: input.action, ...(input.action === "pause" ? { grace_ms: input.graceMs ?? 30_000, escalate: input.escalate ?? null } : { force: input.force ?? false }) };
1059
+ const requestHash = crypto.createHash("sha256").update(JSON.stringify(request)).digest("hex");
1060
+ ensureRunControlStorage(context.db);
1061
+ return context.db.writeTransaction(() => {
1062
+ // Internal SQL-only metadata can be published atomically with the intent,
1063
+ // before any detached control worker observes it. A throw rolls back both.
1064
+ const publish = (receipt) => { onRecorded?.(receipt); return receipt; };
1065
+ const existing = context.db.get("SELECT * FROM run_controls WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, input.runId, input.requestId]);
1066
+ if (existing) {
1067
+ if (existing.request_hash !== requestHash)
1068
+ throw new AppError("runtime_request_conflict", "Control request id has different inputs", 1);
1069
+ return publish({ ok: true, control_id: existing.control_id, recovery_id: existing.recovery_id, action: existing.action, status: existing.status, reused: true });
1070
+ }
1071
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
1072
+ if (!run)
1073
+ throw new AppError("not_found", "RUN is not registered", 1);
1074
+ // Terminal subject state does not prove that its native tree is closed.
1075
+ // Stop still needs the same fenced settlement/capture as a running RUN.
1076
+ if (input.action === "pause" && ["done", "failed", "cancelled"].includes(run.status)) {
1077
+ return { ok: true, terminal: true, status: run.status, control_id: null };
1078
+ }
1079
+ const prior = recoveryGuard(context, project.id, input.runId);
1080
+ const active = context.db.get("SELECT * FROM run_controls WHERE project_id = ? AND run_id = ? AND status IN ('requested','settled','resuming')", [project.id, input.runId]);
1081
+ const strongerStop = active?.action === "stop" && input.force === true && JSON.parse(active.request_json).force !== true;
1082
+ const escalates = active && (active.action === "pause" || strongerStop || active.status === "resuming" || prior?.status === "resuming") && input.action === "stop" && prior?.recovery_id === active.recovery_id && (["draining", "sealed", "resuming"].includes(prior.status) || active.status === "resuming" && prior.status === "recovered");
1083
+ if (active && !escalates)
1084
+ throw new AppError("run_control_in_progress", "RUN already has an unresolved control intent", 1, { control_id: active.control_id });
1085
+ if (prior && ["draining", "sealed", "resuming"].includes(prior.status) && !escalates)
1086
+ throw new AppError("run_recovery_guarded", "Existing recovery must be reconciled before another control intent", 1);
1087
+ const now = context.now();
1088
+ if (escalates) {
1089
+ context.db.run("UPDATE run_controls SET status = 'superseded', updated_at = ? WHERE control_id = ?", [now, active.control_id]);
1090
+ context.db.run("UPDATE run_recovery_guards SET status = 'abandoned', updated_at = ? WHERE recovery_id = ?", [now, prior.recovery_id]);
1091
+ context.db.run("UPDATE run_recovery_bindings SET status = 'abandoned' WHERE recovery_id = ? AND status = 'prepared'", [prior.recovery_id]);
1092
+ }
1093
+ const controlId = `CTL-${crypto.randomUUID()}`;
1094
+ const recoveryId = `RCV-${crypto.randomUUID()}`;
1095
+ const generation = (prior?.generation ?? 0) + 1;
1096
+ context.db.run("INSERT INTO run_recovery_guards (recovery_id, project_id, run_id, generation, status, interruption_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'draining', ?, ?, ?)", [recoveryId, project.id, input.runId, generation, JSON.stringify({ control_id: controlId, action: input.action }), now, now]);
1097
+ context.db.run("INSERT INTO run_controls (control_id, project_id, run_id, request_id, request_hash, request_json, recovery_id, action, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'requested', ?, ?)", [controlId, project.id, input.runId, input.requestId, requestHash, JSON.stringify(request), recoveryId, input.action, now, now]);
1098
+ context.db.run("INSERT INTO run_control_admissions (control_id, work_session_id, work_id, session_id, admitted_at) SELECT ?, ws.id, w.work_id, ws.session_id, ? 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' AND w.status = 'running'", [controlId, now, project.id, input.runId]);
1099
+ context.db.run(`INSERT INTO run_control_stage_admissions (control_id, stage, attempt, work_session_id)
1100
+ SELECT ?, json_extract(stage.value, '$.stage'), COALESCE(json_extract(stage.value, '$.attempt'), 'try-001'),
1101
+ (SELECT a.work_session_id FROM run_control_admissions a JOIN sessions s ON s.session_id = a.session_id AND s.project_id = r.project_id JOIN works w ON w.work_id = a.work_id
1102
+ WHERE a.control_id = ? AND s.current_stage = json_extract(stage.value, '$.stage') ORDER BY (w.parent_work_id IS NULL) DESC, a.work_session_id LIMIT 1)
1103
+ FROM runs r, json_each(r.index_json, '$.stage_runs') stage
1104
+ WHERE r.project_id = ? AND r.id = ? AND json_extract(stage.value, '$.status') = 'running'
1105
+ AND EXISTS (SELECT 1 FROM run_control_admissions a JOIN sessions s ON s.session_id = a.session_id AND s.project_id = r.project_id WHERE a.control_id = ? AND s.current_stage = json_extract(stage.value, '$.stage'))`, [controlId, controlId, project.id, input.runId, controlId]);
1106
+ setFlowRunRecoveryPaused(context, project.id, input.runId, true);
1107
+ return publish({ ok: true, control_id: controlId, recovery_id: recoveryId, generation, action: input.action, status: "requested", settled: false });
1108
+ });
1109
+ }