@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155

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 +23 -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 +273 -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 +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  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 +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  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 +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  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 +214 -24
  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 -4
  190. package/dist/services/retained-native-hook.js +245 -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 +12 -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 +209 -43
  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 +9 -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 +252 -66
  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 +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -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,14 @@ 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";
56
58
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
59
+ import { recordTechnicalNote } from "./technical-notes.js";
60
+ import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
57
61
  export { resumeRunController } from "./run-controller-recovery.js";
58
62
  const preparedLaunchInputs = new WeakMap();
59
63
  function launchInputDigest(input) {
@@ -113,7 +117,7 @@ export function prepareRunControllerLaunch(context, input) {
113
117
  }
114
118
  /** Retain the existing launch intent and frozen inputs without starting a
115
119
  * process. Dispatch remains exclusively owned by launchRunController. */
116
- export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
120
+ export async function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
117
121
  const selection = preparedLaunchInputs.get(prepared);
118
122
  if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
119
123
  throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
@@ -126,7 +130,14 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
126
130
  return { controller: current, reused: true };
127
131
  }
128
132
  const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
129
- assertRunMutationAllowed(context, project.id, run.id, generation);
133
+ const assertCurrent = () => {
134
+ if (selection.sha256 !== launchInputDigest(input))
135
+ throw new AppError("runtime_request_conflict", "Controller launch changed during physical observation", 1, { effect: "no_effect" });
136
+ assertRunMutationAllowed(context, project.id, run.id, generation);
137
+ if (requireRun(context, project.id, run.id).status !== "running")
138
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
139
+ };
140
+ const launcher = await physicalStartup(context.databaseDeadlineAt).birth(assertCurrent);
130
141
  ensureRunControllerStorage(context.db);
131
142
  const controllerId = `DRV-${crypto.randomUUID()}`;
132
143
  const root = path.join(run.run_root, "controllers", controllerId);
@@ -145,8 +156,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
145
156
  const token = crypto.randomUUID();
146
157
  const now = context.now();
147
158
  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
159
  const duplicate = context.db.writeTransaction(() => {
160
+ assertCurrent();
150
161
  if (routing.bundle)
151
162
  assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
152
163
  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 +182,7 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
171
182
  return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
172
183
  }
173
184
  export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
174
- const retained = retainRunControllerLaunch(context, input, prepared);
185
+ const retained = await retainRunControllerLaunch(context, input, prepared);
175
186
  // A simultaneous publisher already owns dispatch. Preserve the original
176
187
  // race behavior: only an explicit replay may reconcile that existing owner.
177
188
  if (prepared.kind === "create" && retained.reused)
@@ -216,7 +227,7 @@ export function runControllerStatus(context, input) {
216
227
  if (!row)
217
228
  return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
218
229
  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 };
230
+ 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
231
  }
221
232
  export async function serveRunController(context, input) {
222
233
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -305,7 +316,7 @@ export async function serveRunController(context, input) {
305
316
  await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
306
317
  await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
307
318
  await mergeLease.ensureConfirmed(assertLeaseFence); });
308
- return controllerReceipt(requireController(context, row.controller_id));
319
+ return controllerReceipt(requireController(context, row.controller_id), context);
309
320
  }
310
321
  catch (error) {
311
322
  primaryFailure = error;
@@ -372,8 +383,7 @@ export async function serveRunController(context, input) {
372
383
  finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
373
384
  }
374
385
  catch (cleanup) {
375
- if (!primaryFailure)
376
- throw cleanup;
386
+ 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
387
  retainSecondary("finish_controller_process", cleanup);
378
388
  }
379
389
  }
@@ -546,6 +556,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
546
556
  };
547
557
  let lastCapture = null;
548
558
  let terminalFinalization = false;
559
+ let technicalPauseRequested = false;
549
560
  let fatal;
550
561
  let stopping;
551
562
  const cancellation = new AbortController();
@@ -603,6 +614,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
603
614
  return Promise.resolve();
604
615
  if (retainControllerLeaseUncertainty(context, row, error))
605
616
  return Promise.resolve();
617
+ 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)
618
+ return pauseDependency(state.sessions[state.current_session]).catch(cause => {
619
+ if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
620
+ return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
621
+ });
606
622
  if (stopping)
607
623
  return stopping;
608
624
  fatal = error;
@@ -637,7 +653,49 @@ async function executeController(context, row, manifest, state, assertOwnership,
637
653
  }
638
654
  });
639
655
  };
640
- const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
656
+ const call = async (session, args, productive = true, operationId) => {
657
+ const receipt = await controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, operationId, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
658
+ const previous = state.last_receipt;
659
+ // A read refreshes native observations, not the already accepted engine
660
+ // boundary. Never copy native readiness/settlement fields from the old read.
661
+ return args[0] === "session" && args[1] === "inspect" && previous?.accepted_boundary && adapterSessionId(previous) === session.id
662
+ ? { ...receipt, accepted_boundary: previous.accepted_boundary } : receipt;
663
+ };
664
+ const note = (session, code, phase, consequence, operationId, noteState) => recordTechnicalNote(context, row.project_id, row.run_id, {
665
+ code, message: "The accepted result is retained independently of native session retirement or observation.", phase,
666
+ generation: row.generation, ...(state.stage ? { stage: state.stage } : {}), harness: runtimeHarness(session.profile),
667
+ resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence, ...(operationId ? { source_operation_id: operationId } : {}), ...(noteState ? { state: noteState } : {})
668
+ });
669
+ const failedChildren = (session) => (childObserver(session)?.failed() ?? []).filter(child => {
670
+ if (!acceptedChildWork(context, row, runtimeHarness(session.profile), session.id, child.session_id))
671
+ return true;
672
+ note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
673
+ return false;
674
+ });
675
+ const pauseDependency = async (session, sourceOperationId) => {
676
+ assertOwner();
677
+ note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
678
+ const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
679
+ const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
680
+ // Publish source binding in the same SQL transaction as the control
681
+ // intent. A detached worker must never observe an unlabelled pause.
682
+ const intent = recordRunControlIntent(executionContext, input, receipt => {
683
+ const current = requireController(context, row.controller_id);
684
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.process_id !== row.process_id)
685
+ throw new AppError("controller_owner_mismatch", "Controller changed before its technical control was recorded", 1);
686
+ const controlId = receipt.control_id;
687
+ if (typeof controlId !== "string")
688
+ return;
689
+ 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 ?? ""]);
690
+ if (!prior)
691
+ 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 } : {}) });
692
+ });
693
+ if (typeof intent.control_id !== "string")
694
+ throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
695
+ technicalPauseRequested = true;
696
+ const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
697
+ throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
698
+ };
641
699
  const confirmStopped = async (stateDir, receipt) => {
642
700
  const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
643
701
  const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
@@ -661,25 +719,75 @@ async function executeController(context, row, manifest, state, assertOwnership,
661
719
  saveState(context, row, state);
662
720
  appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
663
721
  };
722
+ const retireSession = async (session) => {
723
+ session.retired = true;
724
+ saveState(context, row, state);
725
+ try {
726
+ await stopSession(session);
727
+ session.cleanup_pending = false;
728
+ note(session, "native_session_cleanup_pending", "native_cleanup", "none", undefined, "resolved");
729
+ return true;
730
+ }
731
+ catch {
732
+ session.cleanup_pending = true;
733
+ saveState(context, row, state);
734
+ note(session, "native_session_cleanup_pending", "native_cleanup", "cleanup_pending");
735
+ // Full cleanup remains truthful. Only physical absence or an already
736
+ // accepted native execution-settlement proof permits conflicting reuse.
737
+ try {
738
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
739
+ if (managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id }))
740
+ return true;
741
+ return managedExecutionReusable(binding.state, runtimeHarness(session.profile));
742
+ }
743
+ catch {
744
+ return false;
745
+ }
746
+ }
747
+ };
664
748
  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;
749
+ await Promise.all(state.sessions.map(retireSession));
750
+ };
751
+ const captureBoundary = async (stage, nextStage) => {
752
+ try {
753
+ await captureControllerBoundary(executionContext, row, manifest, stage, nextStage, assertPhysicalOwner);
754
+ }
755
+ catch (error) {
756
+ const code = String(error?.code ?? "capture_unavailable");
757
+ if (["controller_owner_mismatch", "controller_capture_owner_lost", "run_recovery_guarded", "run_recovery_generation_stale"].includes(code))
758
+ throw error;
759
+ 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" });
760
+ 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]);
761
+ // A missing report is not a failed Stage. A real unstable source still
762
+ // fences its dependent successor until the existing control can seal it.
763
+ if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
764
+ await pauseDependency(state.sessions[state.current_session]);
765
+ }
669
766
  };
670
767
  const reconcilePromptPredecessor = async (session) => {
671
768
  const prior = state.last_receipt?.settlement;
769
+ const accepted = state.last_receipt?.accepted_boundary;
672
770
  if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
673
771
  return;
674
- const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
772
+ const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
773
+ if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
774
+ await pauseDependency(session);
775
+ throw error;
776
+ });
675
777
  state.last_receipt = receipt;
676
778
  saveState(context, row, state);
677
779
  const current = receipt.settlement;
678
780
  if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
781
+ if (accepted && controllerObligationAccepted(context, row, accepted)) {
782
+ note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
783
+ if (receipt.native_settled === true && receipt.settled === true)
784
+ return;
785
+ await pauseDependency(session);
786
+ }
679
787
  throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
680
788
  }
681
789
  };
682
- const prompt = async (session, text, operationId, kind) => {
790
+ const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
683
791
  const hitl = kind === "hitl";
684
792
  const pending = state.pending_capacity;
685
793
  if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
@@ -687,6 +795,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
687
795
  if (!pending)
688
796
  await reconcilePromptPredecessor(session);
689
797
  const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
798
+ const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
690
799
  const hitlText = async () => {
691
800
  const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
692
801
  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 +841,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
732
841
  const dispatch = JSON.parse(retained.input_json);
733
842
  if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
734
843
  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);
844
+ return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease, JSON.parse(retained.input_json).obligation);
736
845
  }
737
846
  const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
738
847
  fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
739
848
  state.turns += 1;
740
849
  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);
850
+ 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
851
  };
743
852
  let receipt;
744
853
  try {
@@ -754,8 +863,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
754
863
  throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
755
864
  throw JSON.parse(root.error_json);
756
865
  }
757
- const failedChildren = childObserver(session)?.failed() ?? [];
758
- if (failedChildren.length && !(kind === "child" && childCapacityDispatchAllowed(state, failedChildren, session.id, firstOperationId, digestBytes(text))))
866
+ const failures = failedChildren(session);
867
+ if (failures.length && !(kind === "child" && childCapacityDispatchAllowed(state, failures, session.id, firstOperationId, digestBytes(text))))
759
868
  throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
760
869
  promptPending = true;
761
870
  observedPromptOperation = firstOperationId;
@@ -834,14 +943,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
834
943
  await childObserver(session)?.drain();
835
944
  childObserver(session)?.completePostTurn(receipt);
836
945
  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 });
946
+ if (observationGap) {
947
+ if (expectedBoundary && controllerObligationAccepted(context, row, expectedBoundary))
948
+ note(session, "accepted_boundary_observation_gap", "native_observation", "cleanup_pending", firstOperationId);
949
+ else
950
+ 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 });
951
+ }
839
952
  if (fatal)
840
953
  throw fatal;
841
954
  }
842
955
  catch (error) {
843
956
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
844
957
  throw error;
958
+ if (!fatal && lateNativeFailure(error, session.id))
959
+ await pauseDependency(session, firstOperationId);
845
960
  await stopAfterFailure(error);
846
961
  if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
847
962
  cancellation.abort();
@@ -867,6 +982,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
867
982
  monitor.unref();
868
983
  try {
869
984
  for (;;) {
985
+ if (technicalPauseRequested)
986
+ return;
870
987
  // Renewal touches the resource registry; never do it from a callback that
871
988
  // may already own the RUN writer.
872
989
  await renewLease();
@@ -901,14 +1018,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
901
1018
  }
902
1019
  }
903
1020
  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);
1021
+ if (state.current_session !== null && (completed || next.stage !== state.stage)) {
1022
+ const session = state.sessions[state.current_session], failures = failedChildren(session);
1023
+ if (failures.length && !completed)
1024
+ throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids advancing an unaccepted Stage", 1);
1025
+ // The Stage gate is authoritative even for a native helper with no Work
1026
+ // association. This does not claim that helper succeeded or stopped:
1027
+ // normal Session retirement/predecessor reconciliation still fences writes.
1028
+ for (const child of failures)
1029
+ note(session, "accepted_stage_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
1030
+ }
906
1031
  if (completed && manifest.request.stop_after === state.stage) {
907
1032
  // A bounded run is final evidence, not an entry fixture for its successor.
908
1033
  // Stop owned observers before capture; keep the controller lease/fence.
909
1034
  await settle();
910
1035
  if (manifest.request.capture_root)
911
- await captureControllerBoundary(executionContext, row, manifest, completed, null, assertPhysicalOwner);
1036
+ await captureBoundary(completed, null);
912
1037
  setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
913
1038
  setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
914
1039
  return;
@@ -916,7 +1041,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
916
1041
  if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
917
1042
  const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
918
1043
  if (key !== lastCapture) {
919
- await captureControllerBoundary(executionContext, row, manifest, completed, next.kind === "start_stage" ? next.stage : null, assertPhysicalOwner);
1044
+ await captureBoundary(completed, next.kind === "start_stage" ? next.stage : null);
920
1045
  lastCapture = key;
921
1046
  }
922
1047
  }
@@ -1016,7 +1141,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1016
1141
  if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
1017
1142
  const current = state.current_session === null ? null : state.sessions[state.current_session];
1018
1143
  if (current && !current.stopped)
1019
- await stopSession(current);
1144
+ await retireSession(current);
1020
1145
  throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
1021
1146
  }
1022
1147
  };
@@ -1026,8 +1151,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
1026
1151
  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
1152
  }
1028
1153
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
1029
- if (session)
1030
- await stopSession(session);
1154
+ if (session && !await retireSession(session))
1155
+ await pauseDependency(session);
1031
1156
  await renewLease();
1032
1157
  assertOwner();
1033
1158
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
@@ -1072,9 +1197,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
1072
1197
  }
1073
1198
  const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
1074
1199
  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`);
1200
+ 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
1201
  await assertRoute();
1076
1202
  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));
1203
+ await prompt(session, controllerStageEntryPrompt(next.stage, command), undefined, undefined, "entry");
1078
1204
  continue;
1079
1205
  }
1080
1206
  if (state.current_session === null)
@@ -1109,7 +1235,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1109
1235
  if (reconcileChildCapacity(state, children, session.id))
1110
1236
  saveState(context, row, state);
1111
1237
  childObserver(session)?.observeSettledChildren(children);
1112
- const retainedFailures = childObserver(session)?.failed() ?? [];
1238
+ const retainedFailures = failedChildren(session);
1113
1239
  children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
1114
1240
  const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
1115
1241
  if (closedChildEpisodes.length)
@@ -1117,7 +1243,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1117
1243
  saveState(context, row, state);
1118
1244
  appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
1119
1245
  });
1120
- const queuedChild = children.find(child => state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
1246
+ 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
1247
  && (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
1122
1248
  || child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
1123
1249
  if (queuedChild) {
@@ -1145,12 +1271,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
1145
1271
  saveState(context, row, state);
1146
1272
  continue;
1147
1273
  }
1148
- if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child))) {
1274
+ if (children.some(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && renderNativeChildFollowupInstruction(childHarness, child))) {
1149
1275
  const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1150
1276
  state.last_receipt = inspected;
1151
1277
  saveState(context, row, state);
1152
1278
  children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
1153
- const failed = children.filter(child => child.status === "failed");
1279
+ const failed = children.filter(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1154
1280
  // Freeze each distinct child refusal on its first exact observation,
1155
1281
  // even while a healthy sibling still owns an active native Turn.
1156
1282
  for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
@@ -1268,7 +1394,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1268
1394
  continue;
1269
1395
  }
1270
1396
  }
1271
- const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind);
1397
+ const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
1272
1398
  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 };
1273
1399
  const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
1274
1400
  : await nextControllerFanout(executionContext, fanoutInput);
@@ -1302,6 +1428,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
1302
1428
  throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
1303
1429
  }
1304
1430
  if (fanout.kind === "wait") {
1431
+ if (fanout.reason === "accepted_work_native_cleanup") {
1432
+ const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
1433
+ for (const child of pending) {
1434
+ note(session, "accepted_work_native_child_pending", "native_completion", "cleanup_pending", child.session_id);
1435
+ try {
1436
+ await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
1437
+ }
1438
+ catch {
1439
+ await pauseDependency(session);
1440
+ }
1441
+ }
1442
+ const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
1443
+ const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
1444
+ if (lateNativeFailure(error, session.id))
1445
+ await pauseDependency(session);
1446
+ throw error;
1447
+ });
1448
+ state.last_receipt = observed;
1449
+ saveState(context, row, state);
1450
+ if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
1451
+ await pauseDependency(session);
1452
+ continue;
1453
+ }
1305
1454
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
1306
1455
  await delay(1000);
1307
1456
  state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
@@ -1310,6 +1459,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1310
1459
  }
1311
1460
  setStatus("running");
1312
1461
  if (fanout.kind === "external") {
1462
+ let cleanupConflict = false;
1313
1463
  const results = await Promise.allSettled(fanout.works.map(async (work) => {
1314
1464
  try {
1315
1465
  await renewLease();
@@ -1329,18 +1479,32 @@ async function executeController(context, row, manifest, state, assertOwnership,
1329
1479
  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
1480
  const resolved = JSON.parse(launch.profile_json);
1331
1481
  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 });
1482
+ try {
1483
+ 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` });
1484
+ await confirmStopped(launch.state_dir, stopped);
1485
+ appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
1486
+ }
1487
+ catch {
1488
+ 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" });
1489
+ try {
1490
+ cleanupConflict ||= !managedDaemonExecutionAbsent(executionContext, { stateDir: launch.state_dir, projectId: row.project_id, runId: row.run_id });
1491
+ }
1492
+ catch {
1493
+ cleanupConflict = true;
1494
+ }
1495
+ }
1335
1496
  }
1336
1497
  catch (error) {
1337
- await stopAfterFailure(error);
1498
+ if (!(error instanceof AppError && error.code === "controller_technical_dependency_paused"))
1499
+ await stopAfterFailure(error);
1338
1500
  throw error;
1339
1501
  }
1340
1502
  }));
1341
1503
  const rejected = results.find((result) => result.status === "rejected");
1342
1504
  if (rejected)
1343
1505
  throw fatal ?? rejected.reason;
1506
+ if (cleanupConflict)
1507
+ await pauseDependency(session);
1344
1508
  state.last_continuation = null;
1345
1509
  saveState(context, row, state);
1346
1510
  continue;
@@ -1349,10 +1513,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
1349
1513
  assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
1350
1514
  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
1515
  saveState(context, row, state);
1352
- await prompt(session, fanout.prompt);
1516
+ await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
1353
1517
  }
1354
1518
  }
1355
1519
  catch (error) {
1520
+ if (technicalPauseRequested || error instanceof AppError && error.code === "controller_technical_dependency_paused")
1521
+ return;
1356
1522
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
1357
1523
  throw error;
1358
1524
  let cause = fatal ?? error;
@@ -20,6 +20,8 @@ import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKe
20
20
  import { flowCommand } from "./stage-pause.js";
21
21
  import { materializePendingWorkResults } from "./work-registry.js";
22
22
  import { currentStagePrompt } from "./stage-context.js";
23
+ import { stageGraphScope } from "./stage-work-graph.js";
24
+ import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
23
25
  /** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
24
26
  export class PreparedRecoveryCoordinatorRead {
25
27
  #scope;
@@ -357,6 +359,20 @@ export function acceptRunRecovery(context, input) {
357
359
  throw new AppError("recovery_packet_missing", "Memory recovery lacks retained Stage delivery and frozen coordinator scope", 1);
358
360
  const recoveryRead = coordinatorScope ? PreparedRecoveryCoordinatorRead.prepare(context, coordinatorScope, input.recoveryId, eventKey) : undefined;
359
361
  const preparedCustody = coordinatorScope && bundle ? readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: preparedBinding.work_id, recoveryRead: recoveryRead }) : null;
362
+ // Recovered auxiliary permits inherit the exact replacement controller route,
363
+ // not the old packet's controller token or a fabricated native tool receipt.
364
+ const controllers = coordinatorScope && context.db.get("SELECT 1 FROM sqlite_master WHERE name='run_controllers'") ? context.db.all("SELECT controller_id,owner_token,generation,state_json FROM run_controllers WHERE project_id=? AND run_id=? AND status NOT IN ('completed','cancelled','superseded')", [project.id, run.id]) : [];
365
+ const controller = controllers.length === 1 ? controllers[0] : undefined;
366
+ const controllerState = controller && JSON.parse(controller.state_json);
367
+ const currentSession = controllerState && controllerState.current_session !== null ? controllerState.sessions[controllerState.current_session] : undefined;
368
+ let recoveredOwner = null;
369
+ if (controller && currentSession && !currentSession.retired && !currentSession.stopped) {
370
+ const native = requireManagedDaemonBinding(context, { stateDir: currentSession.state_dir, projectId: project.id, runId: run.id, ownerId: controller.controller_id }).state;
371
+ const mapped = native.sessions?.some(item => typeof item === "string" ? item === identity.providerSessionId && item === currentSession.id
372
+ : item.adapter_session_id === currentSession.id && item.provider_session_id === identity.providerSessionId && (item.root_provider_session_id ?? item.provider_session_id) === identity.providerSessionId);
373
+ if (mapped && native.daemon_id === identity.daemonId)
374
+ recoveredOwner = { controller_id: controller.controller_id, controller_owner: { token: controller.owner_token, session_id: currentSession.id } };
375
+ }
360
376
  const result = context.db.writeTransaction(() => {
361
377
  assertRecoveryRunCanResume(context, project.id, run.id);
362
378
  const guard = recoveryGuard(context, project.id, run.id);
@@ -364,6 +380,8 @@ export function acceptRunRecovery(context, input) {
364
380
  const hook = context.db.get("SELECT status, match_key FROM hook_events WHERE id = ?", [identity.hookEventId]);
365
381
  if (!guard || guard.recovery_id !== input.recoveryId || !binding || binding.session_id !== identity.sessionId || binding.harness !== identity.harness || binding.provider_session_id !== identity.providerSessionId || binding.daemon_id !== identity.daemonId || !hook?.match_key || !matchKey.includes(hook.match_key))
366
382
  throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement does not belong to the authorized physical Session and generation", 1);
383
+ if (recoveredOwner && !context.db.get("SELECT controller_id FROM run_controllers WHERE controller_id=? AND project_id=? AND run_id=? AND owner_token=? AND generation=? AND state_json=?", [recoveredOwner.controller_id, project.id, run.id, recoveredOwner.controller_owner.token, guard.generation, controller.state_json]))
384
+ throw new AppError("recovery_owner_changed", "Replacement controller changed before recovery acceptance", 1);
367
385
  const packetReceipt = recoveryPacketReceipt(binding);
368
386
  const retained = packetReceipt.packet;
369
387
  const authorityPrompt = retained.lifecycle_delivery?.text ?? retained.original_prompt.text;
@@ -387,7 +405,7 @@ export function acceptRunRecovery(context, input) {
387
405
  throw new AppError("trusted_recovery_acceptance_required", "Recovery requires a fresh native acknowledgement before work can continue", 1);
388
406
  const packet = JSON.parse(binding.packet_json);
389
407
  const boundary = packet.continuation === "stage_boundary";
390
- const prior = context.db.get("SELECT ws.prompt_path, ws.result_path FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
408
+ const prior = context.db.get("SELECT ws.prompt_path, ws.result_path,ws.start_receipt_json,ws.start_publication_status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
391
409
  if (!prior)
392
410
  throw new AppError("recovery_owner_changed", "Retained root WorkSession changed before recovery acceptance", 1);
393
411
  let physicalBinding = null;
@@ -407,7 +425,18 @@ export function acceptRunRecovery(context, input) {
407
425
  if (!boundary) {
408
426
  const recoveredWorkSessionId = `WS-${crypto.randomUUID()}`;
409
427
  context.db.run("UPDATE work_sessions SET status = 'interrupted', completed_at = ?, updated_at = ? WHERE id = ?", [now, now, binding.prior_work_session_id]);
410
- context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)", [recoveredWorkSessionId, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, now, now]);
428
+ const recoveredReceipt = { ...(prior.start_receipt_json ? JSON.parse(prior.start_receipt_json) : {}), generation: guard.generation,
429
+ prompt: authorityPrompt, prompt_sha256: crypto.createHash("sha256").update(authorityPrompt).digest("hex"), recovered_from_work_session_id: binding.prior_work_session_id,
430
+ recovery_id: guard.recovery_id, launch_hook_event_key: eventKey };
431
+ context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path,start_receipt_json,start_publication_status, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?)", [recoveredWorkSessionId, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, JSON.stringify(recoveredReceipt), "published", now, now]);
432
+ if (coordinatorScope)
433
+ appendAudit(context, { projectId: project.id, eventType: "lifecycle.coordinator_packet", payload: {
434
+ run_id: run.id, work_session_id: recoveredWorkSessionId, packet_sha256: recoveredReceipt.prompt_sha256, generation: guard.generation,
435
+ stage: coordinatorScope.stage, stage_scope: stageGraphScope(context, project.id, run.id, coordinatorScope.stage),
436
+ ...recoveredOwner, daemon_id: identity.daemonId, root_session_id: identity.providerSessionId,
437
+ auxiliary_operations: ["stage_block", ...(coordinatorScope.stage === "code" ? ["work_repair_add"] : [])],
438
+ recovery_id: guard.recovery_id, prompt: authorityPrompt
439
+ } });
411
440
  if (physicalBinding)
412
441
  appendAudit(context, { projectId: project.id, eventType: "stage.coordinator_binding", payload: recoveredCoordinatorBinding(physicalBinding, {
413
442
  projectId: project.id, runId: run.id, stage: coordinatorScope.stage, workId: binding.work_id,