@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -1,5 +1,6 @@
1
- import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
2
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
1
+ import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
2
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
3
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
3
4
  export function recoveryPacketMatchesStage(packet, scope) {
4
5
  return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
5
6
  && packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
@@ -28,10 +29,10 @@ import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
28
29
  import { selectControllerExecution } from "./controller-execution.js";
29
30
  import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
30
31
  import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
31
- import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
32
+ import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess, processTreeIsAlive, resourceHome } from "./managed-processes.js";
32
33
  import { managedLeaseMonitor } from "./managed-lease-monitor.js";
33
34
  import { physicalStartup } from "./physical-startup.js";
34
- import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
35
+ import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecutionAbsent, managedExecutionReusable } from "./managed-daemon-binding.js";
35
36
  import { initCodexHome } from "./hooks.js";
36
37
  import { launchExternalWork } from "./external-work-launch.js";
37
38
  import { runControllerMemoryTask } from "./controller-memory-task.js";
@@ -49,11 +50,16 @@ import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload,
49
50
  import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
50
51
  import { captureControllerBoundary } from "./run-controller-capture.js";
51
52
  import { requestRunControl } from "./run-control-worker.js";
53
+ import { recordRunControlIntent } from "./run-control.js";
52
54
  import { storageSessionId } from "./session-identity.js";
53
55
  import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhausted as overloadExhausted, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, normalizeRecoveryFailure } from "../harness-runtime/lib/provider-overload-policy.mjs";
54
56
  import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
55
57
  import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
58
+ import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
59
+ import { getVnextFanoutStatus } from "./vnext-fanout.js";
56
60
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
61
+ import { recordTechnicalNote } from "./technical-notes.js";
62
+ import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
57
63
  export { resumeRunController } from "./run-controller-recovery.js";
58
64
  const preparedLaunchInputs = new WeakMap();
59
65
  function launchInputDigest(input) {
@@ -113,7 +119,7 @@ export function prepareRunControllerLaunch(context, input) {
113
119
  }
114
120
  /** Retain the existing launch intent and frozen inputs without starting a
115
121
  * process. Dispatch remains exclusively owned by launchRunController. */
116
- export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
122
+ export async function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
117
123
  const selection = preparedLaunchInputs.get(prepared);
118
124
  if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
119
125
  throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
@@ -126,7 +132,14 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
126
132
  return { controller: current, reused: true };
127
133
  }
128
134
  const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
129
- assertRunMutationAllowed(context, project.id, run.id, generation);
135
+ const assertCurrent = () => {
136
+ if (selection.sha256 !== launchInputDigest(input))
137
+ throw new AppError("runtime_request_conflict", "Controller launch changed during physical observation", 1, { effect: "no_effect" });
138
+ assertRunMutationAllowed(context, project.id, run.id, generation);
139
+ if (requireRun(context, project.id, run.id).status !== "running")
140
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
141
+ };
142
+ const launcher = await physicalStartup(context.databaseDeadlineAt).birth(assertCurrent);
130
143
  ensureRunControllerStorage(context.db);
131
144
  const controllerId = `DRV-${crypto.randomUUID()}`;
132
145
  const root = path.join(run.run_root, "controllers", controllerId);
@@ -145,8 +158,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
145
158
  const token = crypto.randomUUID();
146
159
  const now = context.now();
147
160
  const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
148
- const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
149
161
  const duplicate = context.db.writeTransaction(() => {
162
+ assertCurrent();
150
163
  if (routing.bundle)
151
164
  assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
152
165
  const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
@@ -171,7 +184,7 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
171
184
  return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
172
185
  }
173
186
  export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
174
- const retained = retainRunControllerLaunch(context, input, prepared);
187
+ const retained = await retainRunControllerLaunch(context, input, prepared);
175
188
  // A simultaneous publisher already owns dispatch. Preserve the original
176
189
  // race behavior: only an explicit replay may reconcile that existing owner.
177
190
  if (prepared.kind === "create" && retained.reused)
@@ -216,7 +229,7 @@ export function runControllerStatus(context, input) {
216
229
  if (!row)
217
230
  return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
218
231
  const events = context.db.all("SELECT sequence, event_type, data_json, created_at FROM run_controller_events WHERE controller_id = ? AND sequence > ? ORDER BY sequence LIMIT 100", [row.controller_id, input.after ?? 0]);
219
- return { ...controllerReceipt(row), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
232
+ return { ...controllerReceipt(row, context), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
220
233
  }
221
234
  export async function serveRunController(context, input) {
222
235
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -305,7 +318,7 @@ export async function serveRunController(context, input) {
305
318
  await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
306
319
  await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
307
320
  await mergeLease.ensureConfirmed(assertLeaseFence); });
308
- return controllerReceipt(requireController(context, row.controller_id));
321
+ return controllerReceipt(requireController(context, row.controller_id), context);
309
322
  }
310
323
  catch (error) {
311
324
  primaryFailure = error;
@@ -372,8 +385,7 @@ export async function serveRunController(context, input) {
372
385
  finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
373
386
  }
374
387
  catch (cleanup) {
375
- if (!primaryFailure)
376
- throw cleanup;
388
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "controller_resource_retirement_pending", message: "Controller execution ended; resource-registry retirement remains pending.", phase: "controller_cleanup", generation: row.generation, resource_identity: row.process_id, consequence: "cleanup_pending" });
377
389
  retainSecondary("finish_controller_process", cleanup);
378
390
  }
379
391
  }
@@ -546,6 +558,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
546
558
  };
547
559
  let lastCapture = null;
548
560
  let terminalFinalization = false;
561
+ let technicalPauseRequested = false;
549
562
  let fatal;
550
563
  let stopping;
551
564
  const cancellation = new AbortController();
@@ -603,6 +616,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
603
616
  return Promise.resolve();
604
617
  if (retainControllerLeaseUncertainty(context, row, error))
605
618
  return Promise.resolve();
619
+ if (error instanceof AppError && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
620
+ return pauseDependency(state.sessions[state.current_session]).catch(cause => {
621
+ if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
622
+ return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
623
+ });
606
624
  if (stopping)
607
625
  return stopping;
608
626
  fatal = error;
@@ -637,7 +655,51 @@ async function executeController(context, row, manifest, state, assertOwnership,
637
655
  }
638
656
  });
639
657
  };
640
- const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
658
+ const call = async (session, args, productive = true, operationId) => {
659
+ const receipt = await controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, operationId, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
660
+ const previous = state.last_receipt;
661
+ // A read refreshes native observations, not the already accepted engine
662
+ // boundary. Never copy native readiness/settlement fields from the old read.
663
+ return args[0] === "session" && args[1] === "inspect" && previous?.accepted_boundary && adapterSessionId(previous) === session.id
664
+ ? { ...receipt, accepted_boundary: previous.accepted_boundary } : receipt;
665
+ };
666
+ const note = (session, code, phase, consequence, operationId, noteState) => recordTechnicalNote(context, row.project_id, row.run_id, {
667
+ code, message: "The accepted result is retained independently of native session retirement or observation.", phase,
668
+ generation: row.generation, ...(state.stage ? { stage: state.stage } : {}), harness: runtimeHarness(session.profile),
669
+ resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence, ...(operationId ? { source_operation_id: operationId } : {}), ...(noteState ? { state: noteState } : {})
670
+ });
671
+ const failedChildren = (session) => (childObserver(session)?.failed() ?? []).filter(child => {
672
+ if (!acceptedChildWork(context, row, runtimeHarness(session.profile), session.id, child.session_id))
673
+ return true;
674
+ note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
675
+ return false;
676
+ });
677
+ const pauseDependency = async (session, sourceOperationId) => {
678
+ // The control intent contains the retained productive failure; it must not
679
+ // require that same failure to disappear before the owner can pause it.
680
+ assertPhysicalOwner();
681
+ note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
682
+ const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
683
+ const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
684
+ // Publish source binding in the same SQL transaction as the control
685
+ // intent. A detached worker must never observe an unlabelled pause.
686
+ const intent = recordRunControlIntent(executionContext, input, receipt => {
687
+ const current = requireController(context, row.controller_id);
688
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.process_id !== row.process_id)
689
+ throw new AppError("controller_owner_mismatch", "Controller changed before its technical control was recorded", 1);
690
+ const controlId = receipt.control_id;
691
+ if (typeof controlId !== "string")
692
+ return;
693
+ const prior = context.db.get("SELECT 1 FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=? AND COALESCE(json_extract(data_json,'$.source_operation_id'),'')=?", [row.controller_id, controlId, sourceOperationId ?? ""]);
694
+ if (!prior)
695
+ appendEvent(context, row.controller_id, "technical_dependency_paused", { schema_id: "dd-flow/controller-dependency@1", request_id: requestId, control_id: controlId, generation: row.generation, control: receipt, state_dir: session.state_dir, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
696
+ });
697
+ if (typeof intent.control_id !== "string")
698
+ throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
699
+ technicalPauseRequested = true;
700
+ const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
701
+ throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
702
+ };
641
703
  const confirmStopped = async (stateDir, receipt) => {
642
704
  const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
643
705
  const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
@@ -661,25 +723,75 @@ async function executeController(context, row, manifest, state, assertOwnership,
661
723
  saveState(context, row, state);
662
724
  appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
663
725
  };
726
+ const retireSession = async (session) => {
727
+ session.retired = true;
728
+ saveState(context, row, state);
729
+ try {
730
+ await stopSession(session);
731
+ session.cleanup_pending = false;
732
+ note(session, "native_session_cleanup_pending", "native_cleanup", "none", undefined, "resolved");
733
+ return true;
734
+ }
735
+ catch {
736
+ session.cleanup_pending = true;
737
+ saveState(context, row, state);
738
+ note(session, "native_session_cleanup_pending", "native_cleanup", "cleanup_pending");
739
+ // Full cleanup remains truthful. Only physical absence or an already
740
+ // accepted native execution-settlement proof permits conflicting reuse.
741
+ try {
742
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
743
+ if (managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id }))
744
+ return true;
745
+ return managedExecutionReusable(binding.state, runtimeHarness(session.profile));
746
+ }
747
+ catch {
748
+ return false;
749
+ }
750
+ }
751
+ };
664
752
  const settle = async () => {
665
- const results = await Promise.allSettled(state.sessions.map(stopSession));
666
- const failure = results.find((result) => result.status === "rejected");
667
- if (failure)
668
- throw failure.reason;
753
+ await Promise.all(state.sessions.map(retireSession));
754
+ };
755
+ const captureBoundary = async (stage, nextStage) => {
756
+ try {
757
+ await captureControllerBoundary(executionContext, row, manifest, stage, nextStage, assertPhysicalOwner);
758
+ }
759
+ catch (error) {
760
+ const code = String(error?.code ?? "capture_unavailable");
761
+ if (["controller_owner_mismatch", "controller_capture_owner_lost", "run_recovery_guarded", "run_recovery_generation_stale"].includes(code))
762
+ throw error;
763
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "boundary_capture_unavailable", message: "The Stage result is accepted; its assessment snapshot is unavailable.", phase: "assessment_capture", generation: row.generation, stage: stage.stage, ...(manifest.request.capture_root ? { diagnostic_reference: manifest.request.capture_root } : {}), consequence: "none" });
764
+ context.db.run("UPDATE run_controllers SET status='running',last_error_json=NULL,updated_at=? WHERE controller_id=? AND owner_token=? AND generation=? AND status IN ('capturing','capture_failed')", [context.now(), row.controller_id, row.owner_token, row.generation]);
765
+ // A missing report is not a failed Stage. A real unstable source still
766
+ // fences its dependent successor until the existing control can seal it.
767
+ if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
768
+ await pauseDependency(state.sessions[state.current_session]);
769
+ }
669
770
  };
670
771
  const reconcilePromptPredecessor = async (session) => {
671
772
  const prior = state.last_receipt?.settlement;
773
+ const accepted = state.last_receipt?.accepted_boundary;
672
774
  if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
673
775
  return;
674
- const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
776
+ const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
777
+ if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
778
+ await pauseDependency(session);
779
+ throw error;
780
+ });
675
781
  state.last_receipt = receipt;
676
782
  saveState(context, row, state);
677
783
  const current = receipt.settlement;
678
784
  if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
785
+ if (accepted && controllerObligationAccepted(context, row, accepted)) {
786
+ note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
787
+ if (receipt.native_settled === true && receipt.settled === true)
788
+ return;
789
+ await pauseDependency(session);
790
+ }
679
791
  throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
680
792
  }
681
793
  };
682
- const prompt = async (session, text, operationId, kind) => {
794
+ const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
683
795
  const hitl = kind === "hitl";
684
796
  const pending = state.pending_capacity;
685
797
  if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
@@ -687,6 +799,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
687
799
  if (!pending)
688
800
  await reconcilePromptPredecessor(session);
689
801
  const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
802
+ const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
690
803
  const hitlText = async () => {
691
804
  const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
692
805
  const answer = context.db.get("SELECT input_json,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status IN ('accepted','completed')", [answerId, row.controller_id]);
@@ -732,13 +845,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
732
845
  const dispatch = JSON.parse(retained.input_json);
733
846
  if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
734
847
  throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
735
- return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease);
848
+ return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease, JSON.parse(retained.input_json).obligation);
736
849
  }
737
850
  const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
738
851
  fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
739
852
  state.turns += 1;
740
853
  saveState(context, row, state);
741
- return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease);
854
+ return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease, expectedBoundary);
742
855
  };
743
856
  let receipt;
744
857
  try {
@@ -754,8 +867,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
754
867
  throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
755
868
  throw JSON.parse(root.error_json);
756
869
  }
757
- const failedChildren = childObserver(session)?.failed() ?? [];
758
- if (failedChildren.length && !(kind === "child" && childCapacityDispatchAllowed(state, failedChildren, session.id, firstOperationId, digestBytes(text))))
870
+ const failures = failedChildren(session);
871
+ if (failures.length && !(kind === "child" && childCapacityDispatchAllowed(state, failures, session.id, firstOperationId, digestBytes(text))))
759
872
  throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
760
873
  promptPending = true;
761
874
  observedPromptOperation = firstOperationId;
@@ -834,14 +947,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
834
947
  await childObserver(session)?.drain();
835
948
  childObserver(session)?.completePostTurn(receipt);
836
949
  const observationGap = childObserver(session)?.gap();
837
- if (observationGap)
838
- throw new AppError("operation_observation_lost", "In-flight child observation remained unavailable at the safe Turn boundary", 1, { observation_error_code: observationGap, provider_session_id: session.id, stage: state.stage });
950
+ if (observationGap) {
951
+ if (expectedBoundary && controllerObligationAccepted(context, row, expectedBoundary))
952
+ note(session, "accepted_boundary_observation_gap", "native_observation", "cleanup_pending", firstOperationId);
953
+ else
954
+ throw new AppError("operation_observation_lost", "In-flight child observation remained unavailable at the safe Turn boundary", 1, { observation_error_code: observationGap, provider_session_id: session.id, stage: state.stage });
955
+ }
839
956
  if (fatal)
840
957
  throw fatal;
841
958
  }
842
959
  catch (error) {
843
960
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
844
961
  throw error;
962
+ if (!fatal && lateNativeFailure(error, session.id))
963
+ await pauseDependency(session, firstOperationId);
845
964
  await stopAfterFailure(error);
846
965
  if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
847
966
  cancellation.abort();
@@ -867,6 +986,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
867
986
  monitor.unref();
868
987
  try {
869
988
  for (;;) {
989
+ if (technicalPauseRequested)
990
+ return;
870
991
  // Renewal touches the resource registry; never do it from a callback that
871
992
  // may already own the RUN writer.
872
993
  await renewLease();
@@ -901,14 +1022,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
901
1022
  }
902
1023
  }
903
1024
  const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
904
- if (state.current_session !== null && (completed || next.stage !== state.stage) && childObserver(state.sessions[state.current_session])?.failed().length)
905
- throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids completing or advancing its Stage", 1);
1025
+ if (state.current_session !== null && (completed || next.stage !== state.stage)) {
1026
+ const session = state.sessions[state.current_session], failures = failedChildren(session);
1027
+ if (failures.length && !completed)
1028
+ throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids advancing an unaccepted Stage", 1);
1029
+ // The Stage gate is authoritative even for a native helper with no Work
1030
+ // association. This does not claim that helper succeeded or stopped:
1031
+ // normal Session retirement/predecessor reconciliation still fences writes.
1032
+ for (const child of failures)
1033
+ note(session, "accepted_stage_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
1034
+ }
906
1035
  if (completed && manifest.request.stop_after === state.stage) {
907
1036
  // A bounded run is final evidence, not an entry fixture for its successor.
908
1037
  // Stop owned observers before capture; keep the controller lease/fence.
909
1038
  await settle();
910
1039
  if (manifest.request.capture_root)
911
- await captureControllerBoundary(executionContext, row, manifest, completed, null, assertPhysicalOwner);
1040
+ await captureBoundary(completed, null);
912
1041
  setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
913
1042
  setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
914
1043
  return;
@@ -916,7 +1045,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
916
1045
  if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
917
1046
  const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
918
1047
  if (key !== lastCapture) {
919
- await captureControllerBoundary(executionContext, row, manifest, completed, next.kind === "start_stage" ? next.stage : null, assertPhysicalOwner);
1048
+ await captureBoundary(completed, next.kind === "start_stage" ? next.stage : null);
920
1049
  lastCapture = key;
921
1050
  }
922
1051
  }
@@ -1016,7 +1145,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1016
1145
  if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
1017
1146
  const current = state.current_session === null ? null : state.sessions[state.current_session];
1018
1147
  if (current && !current.stopped)
1019
- await stopSession(current);
1148
+ await retireSession(current);
1020
1149
  throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
1021
1150
  }
1022
1151
  };
@@ -1026,8 +1155,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
1026
1155
  throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
1027
1156
  }
1028
1157
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
1029
- if (session)
1030
- await stopSession(session);
1158
+ if (session && !await retireSession(session))
1159
+ await pauseDependency(session);
1031
1160
  await renewLease();
1032
1161
  assertOwner();
1033
1162
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
@@ -1072,9 +1201,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
1072
1201
  }
1073
1202
  const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
1074
1203
  const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
1204
+ bindControllerStageAssignment(executionContext, command, { controllerId: row.controller_id, sessionId: session.id, stateDir: session.state_dir, stage: next.stage, contextSha256: stageContext.sha256, ...(typeof next.attempt === "number" ? { attempt: next.attempt } : {}) });
1075
1205
  await assertRoute();
1076
1206
  appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
1077
- await prompt(session, controllerStageEntryPrompt(next.stage, command));
1207
+ await prompt(session, controllerStageEntryPrompt(next.stage, command), undefined, undefined, "entry");
1078
1208
  continue;
1079
1209
  }
1080
1210
  if (state.current_session === null)
@@ -1109,7 +1239,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1109
1239
  if (reconcileChildCapacity(state, children, session.id))
1110
1240
  saveState(context, row, state);
1111
1241
  childObserver(session)?.observeSettledChildren(children);
1112
- const retainedFailures = childObserver(session)?.failed() ?? [];
1242
+ const retainedFailures = failedChildren(session);
1113
1243
  children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
1114
1244
  const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
1115
1245
  if (closedChildEpisodes.length)
@@ -1117,7 +1247,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1117
1247
  saveState(context, row, state);
1118
1248
  appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
1119
1249
  });
1120
- const queuedChild = children.find(child => state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
1250
+ const queuedChild = children.find(child => !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
1121
1251
  && (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
1122
1252
  || child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
1123
1253
  if (queuedChild) {
@@ -1145,12 +1275,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
1145
1275
  saveState(context, row, state);
1146
1276
  continue;
1147
1277
  }
1148
- if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child))) {
1278
+ if (children.some(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && renderNativeChildFollowupInstruction(childHarness, child))) {
1149
1279
  const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1150
1280
  state.last_receipt = inspected;
1151
1281
  saveState(context, row, state);
1152
1282
  children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1153
- const failed = children.filter(child => child.status === "failed");
1283
+ const failed = children.filter(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1154
1284
  // Freeze each distinct child refusal on its first exact observation,
1155
1285
  // even while a healthy sibling still owns an active native Turn.
1156
1286
  for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
@@ -1268,8 +1398,51 @@ async function executeController(context, row, manifest, state, assertOwnership,
1268
1398
  continue;
1269
1399
  }
1270
1400
  }
1271
- const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind);
1272
- const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 };
1401
+ const repairBlockedWorks = [];
1402
+ if (!retainedFailures.length && children.some(child => child.status === "completed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id))) {
1403
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1404
+ state.last_receipt = inspected;
1405
+ saveState(context, row, state);
1406
+ children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1407
+ // Do not compete with an active parent/healthy sibling or turn a known
1408
+ // repair into a replacement fanout wave.
1409
+ if (inspected.settled !== true || children.some(child => ["running", "unknown"].includes(child.status))) {
1410
+ setStatus("waiting_for_children");
1411
+ await delay(1000, undefined, { signal: cancellation.signal });
1412
+ continue;
1413
+ }
1414
+ let repair = null;
1415
+ const blockers = [];
1416
+ for (const child of children) {
1417
+ try {
1418
+ repair ??= completedChildRepair(executionContext, row, { stage: next.stage, harness: childHarness, parentId: session.id, children: [child] });
1419
+ }
1420
+ catch (error) {
1421
+ if (!(error instanceof AppError) || error.code !== "child_work_repair_blocked")
1422
+ throw error;
1423
+ blockers.push(error);
1424
+ }
1425
+ }
1426
+ if (repair) {
1427
+ reserveChildRepair(executionContext, row, repair, assertOwner);
1428
+ await prompt(session, repair.prompt, repair.operation_id, undefined, "work", [repair.work_id]);
1429
+ continue;
1430
+ }
1431
+ if (blockers.length) {
1432
+ const graph = getVnextFanoutStatus(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage }).orchestration;
1433
+ if (!graph?.works.ready.length)
1434
+ throw blockers[0];
1435
+ for (const blocker of blockers) {
1436
+ const workId = String(blocker.details.work_id);
1437
+ repairBlockedWorks.push(workId);
1438
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
1439
+ phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
1440
+ resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: String(blocker.details.reason) });
1441
+ }
1442
+ }
1443
+ }
1444
+ const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
1445
+ const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0, repairBlockedWorks };
1273
1446
  const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
1274
1447
  : await nextControllerFanout(executionContext, fanoutInput);
1275
1448
  if (memoryFlow)
@@ -1302,6 +1475,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
1302
1475
  throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
1303
1476
  }
1304
1477
  if (fanout.kind === "wait") {
1478
+ if (fanout.reason === "accepted_work_native_cleanup") {
1479
+ const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1480
+ for (const child of pending) {
1481
+ note(session, "accepted_work_native_child_pending", "native_completion", "cleanup_pending", child.session_id);
1482
+ try {
1483
+ await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
1484
+ }
1485
+ catch {
1486
+ await pauseDependency(session);
1487
+ }
1488
+ }
1489
+ const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
1490
+ const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
1491
+ if (lateNativeFailure(error, session.id))
1492
+ await pauseDependency(session);
1493
+ throw error;
1494
+ });
1495
+ state.last_receipt = observed;
1496
+ saveState(context, row, state);
1497
+ if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
1498
+ await pauseDependency(session);
1499
+ continue;
1500
+ }
1305
1501
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
1306
1502
  await delay(1000);
1307
1503
  state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
@@ -1310,6 +1506,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1310
1506
  }
1311
1507
  setStatus("running");
1312
1508
  if (fanout.kind === "external") {
1509
+ let cleanupConflict = false;
1313
1510
  const results = await Promise.allSettled(fanout.works.map(async (work) => {
1314
1511
  try {
1315
1512
  await renewLease();
@@ -1329,18 +1526,32 @@ async function executeController(context, row, manifest, state, assertOwnership,
1329
1526
  throw new AppError("external_work_unsettled", "External launch returned without its terminal Work receipt; observe it rather than relaunching", 1, { work_id: work.work_id, status });
1330
1527
  const resolved = JSON.parse(launch.profile_json);
1331
1528
  const executable = manifest.adapters[runtimeHarness(resolved.worker)].executable;
1332
- const stopped = await runHarnessAdapter({ executable, args: ["daemon", "stop", "--state-dir", launch.state_dir, "--json"], env: controllerEnvironment(context, manifest), operationId: `${row.controller_id}:${work.work_id}:settle` });
1333
- await confirmStopped(launch.state_dir, stopped);
1334
- appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
1529
+ try {
1530
+ const stopped = await runHarnessAdapter({ executable, args: ["daemon", "stop", "--state-dir", launch.state_dir, "--json"], env: controllerEnvironment(context, manifest), operationId: `${row.controller_id}:${work.work_id}:settle` });
1531
+ await confirmStopped(launch.state_dir, stopped);
1532
+ appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
1533
+ }
1534
+ catch {
1535
+ recordTechnicalNote(context, row.project_id, row.run_id, { code: "external_work_cleanup_pending", message: "The Work outcome is committed; its native resources require retirement.", phase: "external_work_cleanup", generation: row.generation, stage: next.stage, work_id: work.work_id, harness: runtimeHarness(resolved.worker), resource_identity: launch.state_dir, diagnostic_reference: launch.state_dir, source_operation_id: `${row.controller_id}:${work.work_id}:settle`, consequence: "cleanup_pending" });
1536
+ try {
1537
+ cleanupConflict ||= !managedDaemonExecutionAbsent(executionContext, { stateDir: launch.state_dir, projectId: row.project_id, runId: row.run_id });
1538
+ }
1539
+ catch {
1540
+ cleanupConflict = true;
1541
+ }
1542
+ }
1335
1543
  }
1336
1544
  catch (error) {
1337
- await stopAfterFailure(error);
1545
+ if (!(error instanceof AppError && error.code === "controller_technical_dependency_paused"))
1546
+ await stopAfterFailure(error);
1338
1547
  throw error;
1339
1548
  }
1340
1549
  }));
1341
1550
  const rejected = results.find((result) => result.status === "rejected");
1342
1551
  if (rejected)
1343
1552
  throw fatal ?? rejected.reason;
1553
+ if (cleanupConflict)
1554
+ await pauseDependency(session);
1344
1555
  state.last_continuation = null;
1345
1556
  saveState(context, row, state);
1346
1557
  continue;
@@ -1349,10 +1560,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
1349
1560
  assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1350
1561
  retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, action: fanout.action, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
1351
1562
  saveState(context, row, state);
1352
- await prompt(session, fanout.prompt);
1563
+ await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
1353
1564
  }
1354
1565
  }
1355
1566
  catch (error) {
1567
+ if (technicalPauseRequested || error instanceof AppError && error.code === "controller_technical_dependency_paused")
1568
+ return;
1356
1569
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
1357
1570
  throw error;
1358
1571
  let cause = fatal ?? error;