@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138

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 (230) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -292,6 +292,34 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
292
292
  const nativeVersions = new Map();
293
293
  for (const [directory, { state, records }] of daemonInventories) {
294
294
  let failedStart = null;
295
+ // Explicit stop can reconcile a retired owner whose last bookkeeping ACK
296
+ // failed. This is physical retirement, not a fabricated clean shutdown.
297
+ if (control?.action === "stop" && state.shutdown_state === "cleanup_failed")
298
+ try {
299
+ const daemonId = state.daemon_id ?? state.config?.daemonId;
300
+ const daemon = records.filter(record => record.kind.endsWith("-daemon"));
301
+ const provider = records.filter(record => record.kind.endsWith("-provider"));
302
+ const operations = retainedDaemonOperations(directory);
303
+ const owner = state.config?.runtime_owner;
304
+ if (daemonId && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))
305
+ && state.active_tree === false && !state.active_operation && state.shutdown?.daemon_id === daemonId
306
+ && state.shutdown.phases?.tree === true && state.shutdown.phases.provider === true
307
+ && daemon.length === 1 && provider.length === 1 && records.length === 2
308
+ && daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token && daemon[0].pid === state.pid
309
+ && provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && provider[0].pid === state.provider_pid
310
+ && JSON.parse(daemon[0].metadata_json).dispatch_barrier === "run-generation@1"
311
+ && records.every(record => record.pid && record.pid_started_at && ["stopped", "failed"].includes(record.state) && record.finished_at && !processTreeIsAlive(record))
312
+ && operations.length > 0 && operations.every(operation => ["completed", "failed", "interrupted"].includes(String(operation.state))
313
+ && (operation.operation === "daemon.start" ? operation.daemon_id === null && operation.launch?.runtime_owner === JSON.stringify(owner)
314
+ : operation.daemon_id === daemonId && ["daemon.stop", "daemon.status", "session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.cancel", "session.inspect"].includes(String(operation.operation))
315
+ && (operation.session_id == null || state.sessions.some(item => (typeof item === "string" ? item : adapterSessionId(item)) === operation.session_id))))) {
316
+ nativeVersions.set(directory, nativeInventoryVersion(directory, operations));
317
+ closedDaemons.set(directory, { source: "observed_owned_retirement", daemon_id: daemonId, stopped: true, settled: true,
318
+ session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)),
319
+ processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })) });
320
+ }
321
+ }
322
+ catch { /* Invalid or unfinished evidence remains on the existing recovery path. */ }
295
323
  if (!interrupt) {
296
324
  try {
297
325
  const nativeOperations = retainedDaemonOperations(directory);
@@ -309,16 +337,48 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
309
337
  const saved = JSON.parse(operation.input_json);
310
338
  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;
311
339
  });
340
+ const cleanup = start?.error?.details;
341
+ const proof = cleanup?.startup_child_cleanup;
342
+ let ownedChildGone = false;
343
+ if (proof && !cleanup?.provider_cleanup_failed && !cleanup?.cleanup_failed && provider.length === 0 && !state.provider_process && state.provider_pid == null
344
+ && daemon.length === 1 && daemon[0].pid === null && daemon[0].pid_started_at === null
345
+ && proof.settled === true && proof.physical_exit === "ESRCH" && proof.daemon_id === state.daemon_id
346
+ && proof.process_id === daemon[0].id
347
+ && proof.pid === state.pid && Number.isSafeInteger(proof.pid) && Number(proof.pid) > 0
348
+ && proof.pgid === (process.platform === "win32" ? null : proof.pid)
349
+ && typeof proof.observed_at === "string" && Number.isFinite(Date.parse(proof.observed_at))
350
+ && proof.owner_id === owner?.owner_id && proof.project_id === project.id && proof.run_id === input.runId
351
+ && proof.operation_id === start?.operation_id && proof.operation_id === owner?.operation_id && proof.state_dir === directory) {
352
+ let leaderGone = false;
353
+ try {
354
+ process.kill(Number(proof.pid), 0);
355
+ }
356
+ catch (error) {
357
+ leaderGone = error.code === "ESRCH";
358
+ }
359
+ if (leaderGone) {
360
+ if (process.platform === "win32")
361
+ ownedChildGone = true;
362
+ else {
363
+ try {
364
+ process.kill(-Number(proof.pgid), 0);
365
+ }
366
+ catch (error) {
367
+ ownedChildGone = error.code === "ESRCH";
368
+ }
369
+ }
370
+ }
371
+ }
312
372
  if (state.shutdown_state === "starting" && state.sessions?.length === 0 && state.active_tree !== true && !state.active_operation
313
373
  && start?.operation === "daemon.start" && start.state === "failed" && start.predecessor_daemon_id === null && start.daemon_id === null
314
374
  && typeof state.daemon_id === "string" && controllerStarts.length === 1 && starts.length === 1 && starts[0].operation_id === start.operation_id
315
375
  && launch?.runtime_owner === JSON.stringify(owner)
316
376
  && daemon.length === 1 && provider.length <= 1 && records.length === daemon.length + provider.length
317
377
  && daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token
318
- && (state.pid == null || state.pid === daemon[0].pid)
378
+ && (state.pid == null || state.pid === daemon[0].pid || ownedChildGone)
319
379
  && (!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)
320
- && records.every(record => record.pid && record.pid_started_at && !processTreeIsAlive(record) && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at))) {
321
- 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 })) };
380
+ && records.every(record => (record.pid && record.pid_started_at && !processTreeIsAlive(record) || ownedChildGone && record === daemon[0]) && ["stopped", "failed"].includes(record.state) && Boolean(record.finished_at))) {
381
+ 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 } : {}) };
322
382
  closedDaemons.set(directory, failedStart);
323
383
  }
324
384
  for (const operation of nativeOperations)
@@ -331,6 +391,8 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
331
391
  unresolved.push({ state_dir: directory, status: "pending", reason: "daemon_operation_inventory_invalid" });
332
392
  }
333
393
  }
394
+ if (closedDaemons.get(directory)?.source === "observed_owned_retirement")
395
+ continue;
334
396
  if (state.shutdown_state !== "clean") {
335
397
  if (state.shutdown_state === "unclean")
336
398
  unresolved.push({ state_dir: directory, status: "pending", reason: "native_shutdown_unclean" });
@@ -634,11 +696,17 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
634
696
  const nativeOperations = retainedDaemonOperations(directory);
635
697
  const targetList = [...targets.values()];
636
698
  const sessionsSettled = ids.every(id => nodes.some((node, index) => targetList[index]?.session.state_dir === directory && targetList[index]?.session.id === id && node.status === "completed"));
637
- // A confirmed failed tree guard admits a new shutdown attempt only after
638
- // fresh tree settlement. Unknown outcomes retain their original ID.
699
+ // A confirmed failure may retry the same incarnation's settled tree or
700
+ // remaining idempotent resource finish. Unknown outcomes keep their ID.
639
701
  if (sessionsSettled) {
702
+ const remainingFinish = state.shutdown_state === "cleanup_failed" && state.active_tree === false
703
+ && state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.tree === true
704
+ && state.shutdown.required_phases?.includes("daemon_resource")
705
+ && state.shutdown.phases.daemon_resource !== true
706
+ && state.shutdown.required_phases.every(phase => phase === "daemon_resource" || state.shutdown.phases[phase] === true);
640
707
  while (nativeOperations.some(operation => operation.operation_id === operationId && operation.daemon_id === daemonId && operation.operation === "daemon.stop"
641
- && operation.state === "failed" && operation.error?.code === "tree_not_settled"))
708
+ && operation.state === "failed" && (operation.error?.code === "tree_not_settled"
709
+ || remainingFinish && operation.error?.code === "process_maintenance_timeout")))
642
710
  operationId += ":finalize";
643
711
  }
644
712
  assertCurrent();
@@ -697,11 +765,13 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
697
765
  const executable = directoryAdapters.get(directory);
698
766
  if (!inventory)
699
767
  continue;
768
+ 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");
769
+ if (!pending.length)
770
+ continue;
700
771
  if (!daemonId || !executable || !node.operation_id) {
701
772
  terminalizedOperations.push({ state_dir: directory, status: "pending", reason: "operation_terminalization_binding_unknown" });
702
773
  continue;
703
774
  }
704
- 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");
705
775
  terminalizedOperations.push(...await Promise.all(pending.map(async (operation) => {
706
776
  try {
707
777
  const operationId = operation.operation_id, sessionId = typeof operation.session_id === "string" ? operation.session_id : null;
@@ -795,7 +865,8 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
795
865
  verifyInventory();
796
866
  }
797
867
  catch (error) {
798
- unresolved.push({ status: "pending", reason: error instanceof AppError && error.code === "owned_inventory_changed" ? error.code : "owned_inventory_recheck_failed" });
868
+ unresolved.push({ status: "pending", reason: error instanceof AppError && error.code === "owned_inventory_changed" ? error.code : "owned_inventory_recheck_failed",
869
+ ...(error instanceof AppError && error.code === "owned_inventory_changed" ? { components: error.details.components } : {}) });
799
870
  }
800
871
  }
801
872
  assertCurrent();
@@ -818,7 +889,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
818
889
  }
819
890
  export function nativeInventoryVersion(directory, operations = retainedDaemonOperations(directory)) {
820
891
  const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
821
- const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
892
+ 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"];
822
893
  // Only these native verbs are read-only. Unknown verbs remain guarded.
823
894
  const productive = operations.filter(operation => !isObservationOperation(operation.operation));
824
895
  return crypto.createHash("sha256").update(JSON.stringify([Object.fromEntries(keys.map(key => [key, state[key]])), productive])).digest("hex");
@@ -6,7 +6,7 @@ import { createEvalRunSnapshot, verifyControllerBoundarySnapshot } from "./eval-
6
6
  import { observeControllerBoundary } from "./run-control.js";
7
7
  import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
8
8
  import { appendEvent, digest, errorRecord, requireController, requireRun } from "./run-controller-state.js";
9
- import { getFlowRunStatus } from "./runs.js";
9
+ import { getFlowRunStatus, readFlowRunExecutionBundle } from "./runs.js";
10
10
  import { recoveryObservationBudget, withRecoveryObservation } from "./recovery-observation-budget.js";
11
11
  import { getResourceDatabase } from "../storage/database.js";
12
12
  import { managedProcessBinding, resourceHome } from "./managed-processes.js";
@@ -60,6 +60,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
60
60
  throw error;
61
61
  }
62
62
  }
63
+ const preparedBundle = readFlowRunExecutionBundle(context, scope);
63
64
  const assertStartOwner = prepareControllerPublication(context, row, assertPhysicalOwner);
64
65
  context.db.writeTransaction(() => {
65
66
  assertStartOwner();
@@ -82,7 +83,7 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
82
83
  if (!current || !["done", "skipped"].includes(current.status) || digest(identity(current)) !== boundaryKey)
83
84
  throw new AppError("controller_boundary_changed", "Stage identity changed after its capture fence", 1);
84
85
  if (physical) {
85
- const continuation = getFlowRunStatus(context, scope).continuation;
86
+ const continuation = getFlowRunStatus(context, scope, preparedBundle ?? undefined).continuation;
86
87
  const stoppedAtTarget = nextStage === null && manifest.request.stop_after === stage.stage && continuation.kind === "start_stage";
87
88
  if (nextStage ? continuation.kind !== "start_stage" || continuation.stage !== nextStage : continuation.kind !== "terminal" && !stoppedAtTarget)
88
89
  throw new AppError("controller_boundary_changed", "Stage successor changed after its capture fence", 1);
@@ -123,7 +124,13 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
123
124
  assertCurrent();
124
125
  context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), context.now(), operationId]);
125
126
  let verify = () => { throw new AppError("controller_boundary_unsettled", "Boundary has no stable writer observation", 1); };
126
- const observation = await withRecoveryObservation(observationBudget, () => observeControllerBoundary(context, { ...scope, controllerId: row.controller_id, operationId, assertCurrent, retainVerification: value => { verify = value; } }));
127
+ const observation = await withRecoveryObservation(observationBudget, () => observeControllerBoundary(context, { ...scope, controllerId: row.controller_id, operationId, assertCurrent, retainVerification: value => {
128
+ verify = () => {
129
+ value();
130
+ if (readFlowRunExecutionBundle(context, scope)?.sha256 !== preparedBundle?.sha256)
131
+ throw new AppError("controller_boundary_changed", "Frozen bundle changed before snapshot publication", 1);
132
+ };
133
+ } }));
127
134
  if (observation.settled === true) {
128
135
  try {
129
136
  createEvalRunSnapshot(context, { ...scope, output, ...(nextStage ? { stageEntry: nextStage } : { candidate: true }), boundary: { controllerId: row.controller_id, operationId, boundaryKey, observation, verify } });
@@ -48,6 +48,22 @@ export function assertControllerRecoveryAllowed(context, row) {
48
48
  || !context.db.get("SELECT 1 FROM run_controls WHERE control_id = ? AND recovery_id = ? AND status = 'resuming'", [state.recovery.control_id, state.recovery.recovery_id]))
49
49
  throw new AppError("run_control_superseded", "Controller launch no longer owns the accepted recovery", 1);
50
50
  }
51
+ /** An admitted physical owner may renew while closing a terminal RUN. This
52
+ * does not admit a new owner or release a recovery/generation fence. */
53
+ export function assertControllerLeaseAllowed(context, row) {
54
+ const current = requireController(context, row.controller_id);
55
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation
56
+ || current.owner_pid !== process.pid || current.process_id !== row.process_id
57
+ || current.process_lease_token !== row.process_lease_token || !current.process_id || !current.process_lease_token
58
+ || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
59
+ throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this physical controller", 1);
60
+ const run = requireRun(context, current.project_id, current.run_id);
61
+ if ((["done", "failed", "cancelled"].includes(run.status) || current.status === "capturing") && launchRecord(context, current)?.value.dispatch_ready === true) {
62
+ assertControllerRecoveryAllowed(context, current);
63
+ return;
64
+ }
65
+ assertLaunchAllowed(context, current);
66
+ }
51
67
  function retire(context, row, identity) {
52
68
  const record = managedProcessStatus(context).find(item => item.id === identity.id);
53
69
  if (!record || record.kind !== "run-controller" || record.owner_id !== row.controller_id || record.project_id !== row.project_id || record.run_id !== row.run_id
@@ -123,6 +139,10 @@ export function claimControllerProcess(context, row) {
123
139
  throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
124
140
  const owner = birth();
125
141
  context.db.writeTransaction(() => {
142
+ // Recovery fences are reconciled by serve's retained failure boundary;
143
+ // only terminal admission must reject before claiming a physical owner.
144
+ if (["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
145
+ throw new AppError("invalid_run_state", "A terminal RUN cannot start a physical controller", 1);
126
146
  const claimed = context.db.run("UPDATE run_controllers SET owner_pid = ?, status = 'running', updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [process.pid, context.now(), row.controller_id, row.owner_token]);
127
147
  if (claimed.changes !== 1)
128
148
  throw new AppError("controller_already_owned", "Managed owner has already claimed this launch", 1);
@@ -142,6 +162,7 @@ export function claimControllerProcess(context, row) {
142
162
  }
143
163
  }
144
164
  export function markControllerDispatchReady(context, row) {
165
+ assertLaunchAllowed(context, row);
145
166
  context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.dispatch_ready', json('true')), updated_at = ? WHERE operation_id = ?", [context.now(), launchId(row)]);
146
167
  }
147
168
  export async function spawnController(context, row) {
@@ -10,6 +10,7 @@ import { verifyRecoverySnapshot } from "./eval-snapshots.js";
10
10
  import { recoveryGuard, resumeRunRecovery } from "./run-recovery.js";
11
11
  import { prepareRunRecoveryDispatch, retainRecoveryCapacityIntent } from "./run-recovery-runtime.js";
12
12
  import { runtimeHarness } from "./execution-policy.js";
13
+ import { selectControllerExecution } from "./controller-execution.js";
13
14
  import { managedProcessBinding, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processTreeIsAlive, processStartedAt } from "./managed-processes.js";
14
15
  import { requireRun, requireController, controllerReceipt, appendEvent, saveState, digest, errorRecord } from "./run-controller-state.js";
15
16
  import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
@@ -74,7 +75,8 @@ export function prepareControllerResume(context, input) {
74
75
  if (!guard || guard.recovery_id !== control?.recovery_id || guard.status !== "sealed" || control.status !== "settled" || !guard.capture_path || JSON.parse(guard.settlement_json ?? "{}").settled !== true)
75
76
  throw new AppError("run_control_not_resumable", "Resume requires the current settled control and its immutable recovery capture", 1);
76
77
  const manifest = JSON.parse(row.manifest_json), state = JSON.parse(row.state_json);
77
- if (!manifest.execution_profile?.settings.execution || !manifest.execution_profile.agent_profiles || !path.isAbsolute(manifest.resource_home) || !path.isAbsolute(manifest.flow_executable))
78
+ selectControllerExecution(context, run, manifest);
79
+ if (!path.isAbsolute(manifest.resource_home) || !path.isAbsolute(manifest.flow_executable))
78
80
  throw new AppError("controller_manifest_invalid", "Resume requires the original frozen controller manifest", 1);
79
81
  if (state.current_session === null || !state.sessions[state.current_session]?.id)
80
82
  throw new AppError("recovery_owner_missing", "Resume requires the retained coordinator Session", 1);
@@ -3,7 +3,7 @@ import { AppError, errorRecord as structuredErrorRecord } from "../shared/errors
3
3
  import { runtimeHarness } from "./execution-policy.js";
4
4
  import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
5
5
  export function requireRun(context, projectId, runId) {
6
- const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, status, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
6
+ const run = context.db.get("SELECT id, project_id, flow_kind, project_root, workspace_root, run_root, status, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
7
7
  if (!run?.run_root)
8
8
  throw new AppError("not_found", "Managed RUN has no materialized root", 1, { run_id: runId });
9
9
  return run;