@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -19,23 +19,25 @@ import { flowCommand } from "./stage-pause.js";
19
19
  import { resolveStageTransition, sameSessionProfile, stageExecutionCwd, runtimeHarness } from "./execution-policy.js";
20
20
  import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
21
21
  import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
22
- import { assertManagedProcessLease, confirmManagedProcess, heartbeatManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
22
+ import { assertManagedProcessLease, confirmManagedProcess, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
23
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
23
24
  import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
24
25
  import { initCodexHome } from "./hooks.js";
25
26
  import { launchExternalWork } from "./external-work-launch.js";
26
27
  import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
27
28
  import { claimQueuedMergeRequest } from "./merge-server.js";
28
29
  import { prepareVnextCodeWorkspace } from "./vnext-code.js";
29
- import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, digest, digestBytes, errorRecord } from "./run-controller-state.js";
30
+ import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
30
31
  import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
31
- import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady } from "./run-controller-process.js";
32
+ import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerRecoveryAllowed } from "./run-controller-process.js";
32
33
  import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
33
- import { capacityDelay, continueCodexCapacity } from "./codex-capacity.js";
34
+ import { capacityDelay, continueCodexCapacity, eligibleCodexChildOverload } from "./codex-capacity.js";
34
35
  import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
35
36
  import { captureControllerBoundary } from "./run-controller-capture.js";
36
37
  import { requestRunControl } from "./run-control-worker.js";
37
38
  import { storageSessionId } from "./session-identity.js";
38
- import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
39
+ import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
40
+ import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
39
41
  export { resumeRunController } from "./run-controller-recovery.js";
40
42
  export function prepareRunControllerLaunch(context, input) {
41
43
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
@@ -134,53 +136,23 @@ export async function launchRunController(context, input, prepared = prepareRunC
134
136
  * proceeding, while an explicit negative renewal remains a hard ownership
135
137
  * loss.
136
138
  */
137
- export function controllerLeaseMonitor(heartbeat) {
138
- let lost = false;
139
- let unconfirmed = null;
140
- let confirmedUntil = 0;
141
- const lostError = () => new AppError("controller_lease_lost", "Managed owner lease could not be confirmed", 1);
142
- const uncertainError = () => new AppError("controller_lease_unconfirmed", "Managed owner lease requires renewal outside the RUN writer", 1);
143
- return {
144
- tick() {
145
- try {
146
- if (!heartbeat())
147
- lost = true;
148
- else {
149
- unconfirmed = null;
150
- confirmedUntil = Date.now() + 14 * 60_000;
151
- }
152
- }
153
- catch (error) {
154
- unconfirmed = error;
155
- }
156
- },
157
- isConfirmed() { return !lost && unconfirmed === null && Date.now() < confirmedUntil; },
158
- assertNoRenewal() {
159
- if (lost)
160
- throw lostError();
161
- if (unconfirmed !== null || Date.now() >= confirmedUntil)
162
- throw uncertainError();
163
- },
164
- assertConfirmed() {
165
- if (lost)
166
- throw lostError();
167
- if (unconfirmed === null && Date.now() < confirmedUntil)
168
- return;
169
- try {
170
- if (!heartbeat()) {
171
- lost = true;
172
- throw lostError();
173
- }
174
- unconfirmed = null;
175
- confirmedUntil = Date.now() + 14 * 60_000;
176
- }
177
- catch (error) {
178
- if (error instanceof AppError)
179
- throw error;
180
- throw new AppError("controller_lease_unconfirmed", "Managed owner lease could not be refreshed", 1, { cause: error instanceof Error ? error.message : String(error) });
181
- }
182
- }
183
- };
139
+ export const controllerLeaseMonitor = managedLeaseMonitor;
140
+ /** Unconfirmed ownership is not evidence authorizing a native cancellation. */
141
+ export function retainControllerLeaseUncertainty(context, row, error) {
142
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
143
+ return false;
144
+ try {
145
+ const db = context.db.withDeadline(Date.now() + 5_000);
146
+ db.writeTransaction(() => {
147
+ const changed = db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND generation = ? AND status NOT IN ('completed','cancelled','superseded','recovery_required','capture_failed')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token, row.generation]);
148
+ if (changed.changes)
149
+ appendEvent({ ...context, db }, row.controller_id, "owner_confirmation_required", { ...errorRecord(error), native_cancelled: false });
150
+ });
151
+ }
152
+ catch (persistence) {
153
+ process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, primary: errorRecord(error), persistence: errorRecord(persistence), native_cancelled: false })}\n`);
154
+ }
155
+ return true;
184
156
  }
185
157
  /** No initialization, provider calls or lease takeover on the observation path. */
186
158
  export function runControllerStatus(context, input) {
@@ -205,25 +177,37 @@ export async function serveRunController(context, input) {
205
177
  if (!row.process_id || !row.process_lease_token)
206
178
  throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
207
179
  confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
208
- const lease = controllerLeaseMonitor(() => heartbeatManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token }));
209
- lease.assertConfirmed();
210
- const heartbeat = setInterval(() => {
211
- try {
212
- lease.tick();
213
- if (lease.isConfirmed()) {
214
- const guard = recoveryGuard(context, row.project_id, row.run_id);
215
- if ((guard?.generation ?? 0) === row.generation && !["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
216
- context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [new Date(Date.now() + 120_000).toISOString(), context.now(), row.project_id, row.run_id, row.controller_id]);
217
- }
180
+ const lease = controllerLeaseMonitor(attemptMs => {
181
+ const deadline = Date.now() + attemptMs;
182
+ const receipt = renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: deadline }, { id: row.process_id, leaseToken: row.process_lease_token });
183
+ if (receipt.owned) {
184
+ // These exact-owner keepalive writes are idempotent. Finish the resource
185
+ // transaction before acquiring RUN writer; both share one attempt budget.
186
+ const db = context.db.withDeadline(deadline);
187
+ if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
188
+ db.writeTransaction(() => {
189
+ const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
190
+ if ((guard?.generation ?? 0) === row.generation && !["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
191
+ db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [new Date(Date.now() + 120_000).toISOString(), context.now(), row.project_id, row.run_id, row.controller_id]);
192
+ });
218
193
  }
219
- catch { /* The lease monitor distinguishes renewal uncertainty from loss. */ }
194
+ return receipt;
195
+ });
196
+ const assertLeaseFence = () => {
197
+ const current = requireController(context, row.controller_id);
198
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation)
199
+ throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this controller generation", 1);
200
+ assertControllerRecoveryAllowed(context, current);
201
+ };
202
+ const heartbeat = setInterval(() => {
203
+ lease.background(assertLeaseFence, () => { });
220
204
  }, 30_000);
221
205
  heartbeat.unref();
222
206
  const manifest = JSON.parse(row.manifest_json);
223
207
  const state = JSON.parse(row.state_json);
224
208
  const assertPhysicalOwner = () => {
225
209
  lease.assertNoRenewal();
226
- assertManagedProcessLease(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
210
+ assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
227
211
  const current = requireController(context, row.controller_id);
228
212
  if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
229
213
  throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
@@ -234,15 +218,16 @@ export async function serveRunController(context, input) {
234
218
  process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, phase, primary: errorRecord(primaryFailure), secondary: errorRecord(error) })}\n`);
235
219
  };
236
220
  try {
221
+ await lease.ensureConfirmed(assertLeaseFence);
237
222
  context.db.writeTransaction(() => {
238
223
  markControllerDispatchReady(context, row);
239
224
  if (!state.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
240
225
  setFlowRunRecoveryPaused(context, row.project_id, row.run_id, false);
241
226
  });
242
227
  appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
243
- if (state.recovery)
228
+ if (state.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
244
229
  await resumeControllerSession(context, row, manifest, state, assertPhysicalOwner);
245
- await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, () => lease.assertConfirmed());
230
+ await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, () => lease.ensureConfirmed(assertLeaseFence));
246
231
  return controllerReceipt(requireController(context, row.controller_id));
247
232
  }
248
233
  catch (error) {
@@ -287,6 +272,7 @@ export async function serveRunController(context, input) {
287
272
  throw error;
288
273
  }
289
274
  finally {
275
+ lease.close();
290
276
  clearInterval(heartbeat);
291
277
  finishOwner();
292
278
  }
@@ -446,6 +432,19 @@ async function executeController(context, row, manifest, state, assertOwnership,
446
432
  const checkOutcomes = () => assertLifecycleOutcomes(observer, { projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
447
433
  const assertOwner = () => { if (fatal)
448
434
  throw fatal; checkOutcomes(); assertOwnership(); };
435
+ const prepareMutation = async (action) => {
436
+ for (;;) {
437
+ await renewLease();
438
+ try {
439
+ context.db.writeTransaction(() => { assertOwner(); action(); });
440
+ return;
441
+ }
442
+ catch (error) {
443
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
444
+ throw error;
445
+ }
446
+ }
447
+ };
449
448
  const policy = manifest.execution_profile.settings.execution;
450
449
  const profiles = manifest.execution_profile.agent_profiles;
451
450
  let executionContext = { ...context, env: { ...context.env, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
@@ -454,10 +453,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
454
453
  projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
455
454
  };
456
455
  let lastCapture = null;
456
+ let terminalFinalization = false;
457
457
  let fatal;
458
458
  let stopping;
459
459
  const cancellation = new AbortController();
460
+ const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
460
461
  const stopAfterFailure = (error) => {
462
+ // A retained successor may already be running. Fencing/recovery records
463
+ // uncertainty; cancellation is not evidence of its missing outcome.
464
+ if (capacityUncertain(error))
465
+ return Promise.resolve();
466
+ if (retainControllerLeaseUncertainty(context, row, error))
467
+ return Promise.resolve();
461
468
  if (stopping)
462
469
  return stopping;
463
470
  fatal = error;
@@ -487,7 +494,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
487
494
  if (changed.changes)
488
495
  appendEvent(context, row.controller_id, "status_changed", { status, stage: state.stage });
489
496
  };
490
- const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined);
497
+ const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
491
498
  const confirmStopped = async (stateDir, receipt) => {
492
499
  const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
493
500
  if (receipt.stopped !== true || receipt.clean !== true)
@@ -531,25 +538,40 @@ async function executeController(context, row, manifest, state, assertOwnership,
531
538
  };
532
539
  const prompt = async (session, text, operationId, kind) => {
533
540
  const hitl = kind === "hitl";
534
- await reconcilePromptPredecessor(session);
535
- const firstOperationId = operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
541
+ const pending = state.pending_capacity;
542
+ if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
543
+ throw new AppError("capacity_continuation_conflict", "Retained capacity owner reference differs from its Session/task", 1);
544
+ if (!pending)
545
+ await reconcilePromptPredecessor(session);
546
+ const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
536
547
  const send = (value, id) => {
537
548
  const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
538
549
  if (retained) {
539
550
  const dispatch = JSON.parse(retained.input_json);
540
551
  if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
541
552
  throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
542
- return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal);
553
+ return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease);
543
554
  }
544
555
  const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
545
556
  fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
546
557
  state.turns += 1;
547
558
  saveState(context, row, state);
548
- return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal);
559
+ return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease);
549
560
  };
550
561
  let receipt;
551
562
  try {
552
563
  try {
564
+ if (pending) {
565
+ const predecessor = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'failed'", [pending.predecessor, row.controller_id, row.generation]);
566
+ if (!predecessor?.error_json)
567
+ throw new AppError("capacity_continuation_conflict", "Retained capacity predecessor has no failure receipt", 1);
568
+ // The root identifies the whole chain; inspect its frozen failure,
569
+ // then reconcile all retained successors before any dispatch.
570
+ const root = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [firstOperationId, row.controller_id, row.generation]);
571
+ if (!root?.error_json)
572
+ throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
573
+ throw JSON.parse(root.error_json);
574
+ }
553
575
  receipt = await send(text, firstOperationId);
554
576
  }
555
577
  catch (error) {
@@ -560,9 +582,16 @@ async function executeController(context, row, manifest, state, assertOwnership,
560
582
  throw error;
561
583
  const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
562
584
  args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
563
- const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true,
564
- retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND operation_id IN (?, ?)", [row.controller_id, row.generation, firstOperationId, `${firstOperationId}:capacity:1:session.prompt`])
565
- .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
585
+ const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
586
+ retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
587
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent?.root_operation_id === firstOperationId ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
588
+ retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
589
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_root === firstOperationId && retained.capacity_failure ? [retained.capacity_failure] : []; }),
590
+ retainFailure: (id, refusal) => {
591
+ const changed = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_root: firstOperationId, capacity_failure: refusal }), context.now(), id, row.controller_id, row.generation]);
592
+ if (changed.changes !== 1)
593
+ throw new AppError("capacity_continuation_blocked", "Failed Turn owner changed before its refusal was retained", 1);
594
+ },
566
595
  inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
567
596
  inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
568
597
  assertCurrent: () => {
@@ -598,14 +627,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
598
627
  }
599
628
  else if (next.kind === "paused")
600
629
  return "paused";
601
- return next.kind === "wait" ? "reconciliation_blocked" : "continue";
630
+ return next.kind === "wait" && !["native_children_unsettled", "work_receipts_pending", "qualified_capacity_required"].includes(next.reason) ? "reconciliation_blocked" : "continue";
602
631
  },
603
- retainIntent: intent => context.db.writeTransaction(() => {
604
- const changed = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
605
- if (changed.changes !== 1)
606
- throw new AppError("capacity_continuation_blocked", "Failed native operation changed before continuation", 1);
607
- appendEvent(context, row.controller_id, "capacity_continuation", intent);
608
- }),
632
+ retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
609
633
  prompt: (id, value) => send(value, id),
610
634
  ...(resumeCommand ? { continuationText: `Continue the same accepted HITL task in this Session. First inspect current durable RUN and pause state. Only if the exact pause still awaits the accepted answer, run this previously issued command once:\n\n${resumeCommand}\n\nNever replay an already committed answer. Otherwise use only the current issued successor.` } : {}) });
611
635
  if (!resumed)
@@ -620,10 +644,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
620
644
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
621
645
  throw error;
622
646
  await stopAfterFailure(error);
623
- cancellation.abort();
647
+ if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
648
+ cancellation.abort();
624
649
  throw fatal ?? error;
625
650
  }
626
651
  state.last_receipt = receipt;
652
+ delete state.pending_capacity;
627
653
  saveState(context, row, state);
628
654
  return receipt;
629
655
  };
@@ -642,14 +668,37 @@ async function executeController(context, row, manifest, state, assertOwnership,
642
668
  for (;;) {
643
669
  // Renewal touches the resource registry; never do it from a callback that
644
670
  // may already own the RUN writer.
645
- renewLease();
671
+ await renewLease();
646
672
  assertOwner();
647
673
  if (state.current_session !== null && !state.sessions[state.current_session].stopped)
648
674
  bindInvocationScope(state.sessions[state.current_session]);
649
675
  const run = requireRun(context, row.project_id, row.run_id);
676
+ if (state.pending_capacity && !["done", "failed", "cancelled"].includes(run.status)) {
677
+ const pending = state.pending_capacity;
678
+ const session = state.sessions.find(item => item.id === pending.session_id && !item.stopped);
679
+ const operation = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [pending.root_operation_id, row.controller_id, row.generation]);
680
+ const dispatch = operation && JSON.parse(operation.input_json);
681
+ const args = dispatch?.args;
682
+ const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
683
+ if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
684
+ throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
685
+ await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
686
+ continue;
687
+ }
650
688
  const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
651
689
  const next = view.continuation;
690
+ terminalFinalization = next.kind === "terminal";
652
691
  const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
692
+ if (completed && manifest.request.stop_after === state.stage) {
693
+ // A bounded run is final evidence, not an entry fixture for its successor.
694
+ // Stop owned observers before capture; keep the controller lease/fence.
695
+ await settle();
696
+ if (manifest.request.capture_root)
697
+ await captureControllerBoundary(executionContext, row, manifest, completed, null, assertPhysicalOwner);
698
+ setStatus("stop_target_reached");
699
+ appendEvent(context, row.controller_id, "stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
700
+ return;
701
+ }
653
702
  if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
654
703
  const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
655
704
  if (key !== lastCapture) {
@@ -657,12 +706,6 @@ async function executeController(context, row, manifest, state, assertOwnership,
657
706
  lastCapture = key;
658
707
  }
659
708
  }
660
- if (completed && manifest.request.stop_after === state.stage) {
661
- await settle();
662
- setStatus("stop_target_reached");
663
- appendEvent(context, row.controller_id, "stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
664
- return;
665
- }
666
709
  if (next.kind === "terminal") {
667
710
  await settle();
668
711
  setStatus(run.status === "cancelled" ? "cancelled" : "completed");
@@ -684,6 +727,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
684
727
  const input = JSON.parse(answer.input_json);
685
728
  if (digestBytes(fs.readFileSync(input.file)) !== input.sha256)
686
729
  throw new AppError("context_checksum_mismatch", "Accepted HITL answer bytes changed", 1);
730
+ await renewLease();
687
731
  assertOwner();
688
732
  // The accepted answer is logical intent; only its generation-bound
689
733
  // adapter operation represents a physical dispatch that stop must drain.
@@ -738,6 +782,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
738
782
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
739
783
  if (session)
740
784
  await stopSession(session);
785
+ await renewLease();
741
786
  assertOwner();
742
787
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
743
788
  fs.mkdirSync(directory, { mode: 0o700 });
@@ -768,12 +813,14 @@ async function executeController(context, row, manifest, state, assertOwnership,
768
813
  }
769
814
  state.native_baseline = controllerNativeChildren(state.last_receipt ?? {}, session.id).map(child => child.session_id);
770
815
  state.stage = next.stage;
816
+ state.child_capacity = {};
771
817
  saveState(context, row, state);
772
818
  setStatus("running");
773
819
  if (next.stage === "code" || next.stage === "code-review") {
774
820
  // A CODE_REVIEW fork has source files but intentionally no installed
775
821
  // dependencies. Its repair Work executes checks just like CODE Work.
776
822
  const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
823
+ await renewLease();
777
824
  assertOwner();
778
825
  appendEvent(context, row.controller_id, "code_workspace_prepared", { stage: next.stage, readiness_status: readiness.status ?? null });
779
826
  }
@@ -794,45 +841,88 @@ async function executeController(context, row, manifest, state, assertOwnership,
794
841
  saveState(context, row, state);
795
842
  children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
796
843
  const failed = children.filter(child => child.status === "failed");
797
- const child = failed.length === 1 && children.every(item => item === failed[0] || item.status === "completed") ? failed[0] : undefined;
844
+ // Freeze each distinct child refusal on its first exact observation,
845
+ // even while a healthy sibling still owns an active native Turn.
846
+ for (const failedChild of failed.filter(item => eligibleCodexChildOverload(item, session.id))) {
847
+ const linked = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: failedChild.session_id })]);
848
+ if (linked.length > 1 || linked.some(link => link.status !== "running"))
849
+ continue;
850
+ const retained = state.child_capacity?.[failedChild.session_id];
851
+ const refusals = [...(retained?.refusals ?? [])];
852
+ if (refusals.some(item => item.turn_id === failedChild.terminal_turn_id))
853
+ continue;
854
+ const nativeFailure = { code: "turn_interrupted", details: { provider_session_id: failedChild.session_id, turn_id: failedChild.terminal_turn_id, native_turn_id: failedChild.terminal_turn_id, terminal_status: "failed", provider_error: failedChild.provider_error, observed_at: failedChild.terminal_observed_at } };
855
+ refusals.push(refusalObservation(nativeFailure, retained?.attempts ?? 0, failedChild.terminal_observed_at ?? context.now()));
856
+ state.child_capacity ??= {};
857
+ state.child_capacity[failedChild.session_id] = { ...retained, turn_id: retained?.turn_id ?? failedChild.terminal_turn_id, attempts: retained?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
858
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_refusal", { child_session_id: failedChild.session_id, refusal: refusals.at(-1) }); });
859
+ const burst = overloadBurst(refusals);
860
+ if (burst)
861
+ throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: failedChild.session_id, primary_error: nativeFailure });
862
+ }
863
+ // Continue one existing child at a time; another failed child does not
864
+ // create a second fanout wave or share this child's refusal counter.
865
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => item.parent_session_id === session.id && item.terminal_turn_id && nativeItemsSettled(item.native_turn_items) && terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: item.session_id, turn_id: item.terminal_turn_id, native_turn_id: item.terminal_turn_id, terminal_status: "failed", provider_error: item.provider_error } }, item.session_id)) : undefined;
798
866
  const turnId = child?.terminal_turn_id;
799
867
  const native = child && turnId ? { code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: turnId, native_turn_id: turnId,
800
- terminal_status: "failed", provider_error: child.provider_error } } : null;
868
+ terminal_status: "failed", provider_error: child.provider_error, observed_at: child.terminal_observed_at } } : null;
801
869
  const prior = child && state.child_capacity?.[child.session_id];
802
870
  const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
803
871
  if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
804
- && child.native_turn_items?.observed === true && (child.native_turn_items.possible_effects === false || child.native_turn_items.pending === false)
872
+ && nativeItemsSettled(child.native_turn_items)
805
873
  && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
806
- && (!links.length || links[0].status === "running") && ((prior?.attempts ?? 0) < 2 || prior?.turn_id === turnId && prior.dispatched === false)) {
874
+ && (!links.length || links[0].status === "running")) {
807
875
  const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
808
- if (prior?.turn_id === turnId && !prepared)
876
+ if (prior?.turn_id === turnId && prior.dispatched === true && !prepared)
809
877
  throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
810
878
  const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
879
+ if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
880
+ throw new AppError("capacity_continuation_conflict", "Native child continuation policy or ordinal is incompatible", 1);
881
+ const refusals = [...(prior?.refusals ?? [])];
882
+ if (!refusals.some(item => item.turn_id === turnId)) {
883
+ const refusal = refusalObservation(native, attempt - 1, child.terminal_observed_at ?? context.now());
884
+ refusals.push(refusal);
885
+ state.child_capacity ??= {};
886
+ state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
887
+ saveState(context, row, state);
888
+ }
889
+ const burst = overloadBurst(refusals);
890
+ if (burst)
891
+ throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: child.session_id, primary_error: native });
811
892
  const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
812
- if (!Number.isFinite(wait) || wait > 30_000)
813
- throw new AppError("capacity_child_retry_delay_exceeded", "Native child retry delay exceeds the bounded continuation window", 1, { child_session_id: child.session_id, delay_ms: wait });
814
- const operationId = prepared?.operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
893
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
894
+ throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
895
+ const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
896
+ const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
815
897
  state.child_capacity ??= {};
816
- state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
817
- saveState(context, row, state);
818
- appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
819
- await delay(wait, undefined, { signal: cancellation.signal });
898
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
899
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId }); });
900
+ const until = performance.now() + wait;
901
+ while (performance.now() < until) {
902
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
903
+ await renewLease();
904
+ assertOwner();
905
+ }
906
+ await renewLease();
820
907
  assertOwner();
821
908
  const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
822
- if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
909
+ if (current.continuation.stage !== state.stage || ["terminal", "paused"].includes(current.continuation.kind))
910
+ continue;
911
+ if (["blocked", "wait"].includes(current.continuation.kind))
823
912
  throw new AppError("capacity_child_followup_blocked", "RUN changed during native child capacity wait", 1, { child_session_id: child.session_id });
824
913
  const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
825
914
  const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
826
915
  const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
827
- if (still?.status === "completed" && currentLinks.length <= 1 && currentLinks.every(link => link.status === "completed")) {
916
+ if (currentLinks.length === 1 && ["completed", "cancelled", "paused"].includes(currentLinks[0].status)) {
828
917
  state.last_receipt = fresh;
829
918
  saveState(context, row, state);
830
919
  continue;
831
920
  }
832
921
  if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
833
- || still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false && still.native_turn_items.pending !== false || currentLinks.length > 1
922
+ || !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
834
923
  || currentLinks.some(link => link.status !== "running"))
835
924
  throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
925
+ await renewLease();
836
926
  assertOwner();
837
927
  const text = `The existing native child ${child.session_id} has a confirmed terminal overloaded Turn. Use the native followup_task tool exactly once for that same child ID with this short message: "Continue your assigned Work from its current durable state. Do not repeat an observed lifecycle command. Finish only the original Work." Do not spawn another child, claim another Work, or perform the child's Work in this parent Turn. End this Turn after the followup_task response.`;
838
928
  await prompt(session, text, operationId, "child");
@@ -859,7 +949,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
859
949
  if (fanout.kind === "wait") {
860
950
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
861
951
  await delay(1000);
862
- state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, "--state-dir", session.state_dir, "--json"], false);
952
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
863
953
  saveState(context, row, state);
864
954
  continue;
865
955
  }
@@ -867,6 +957,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
867
957
  if (fanout.kind === "external") {
868
958
  const results = await Promise.allSettled(fanout.works.map(async (work) => {
869
959
  try {
960
+ await renewLease();
870
961
  assertOwner();
871
962
  const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
872
963
  if (fatal)
@@ -904,8 +995,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
904
995
  catch (error) {
905
996
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
906
997
  throw error;
907
- await stopAfterFailure(fatal ?? error);
908
- throw fatal ?? error;
998
+ let cause = fatal ?? error;
999
+ if (!fatal && terminalFinalization) {
1000
+ const record = errorRecord(error);
1001
+ const provenance = { phase: "terminal_finalization", project_root: manifest.request.project_root, run_id: row.run_id, controller_id: row.controller_id };
1002
+ if (error instanceof AppError)
1003
+ Object.assign(error.details, provenance);
1004
+ else
1005
+ cause = new AppError(String(record.code), String(record.message), 1, { ...record.details, ...provenance, cause: record });
1006
+ }
1007
+ await stopAfterFailure(cause);
1008
+ throw fatal ?? cause;
909
1009
  }
910
1010
  finally {
911
1011
  clearInterval(monitor);
@@ -6,7 +6,7 @@ import { AppError } from "../shared/errors.js";
6
6
  import { canonicalPath } from "../storage/paths.js";
7
7
  import { importGuardPath, rearmCompletedSnapshotImport } from "../storage/import-guard.js";
8
8
  import { installCurrentEngine, upgradeForkedRunEngine, prepareForkEngine } from "./engines.js";
9
- import { restoreEvalRunSnapshot, prepareEvalRunSnapshot, prepareEvalRunRestore, restoredWorkspaceRoot } from "./eval-snapshots.js";
9
+ import { restoreEvalRunSnapshot, prepareEvalRunSnapshot, prepareEvalRunRestore, prepareSnapshotOperationalDecision, restoredWorkspaceRoot } from "./eval-snapshots.js";
10
10
  export function prepareRunFork(input) {
11
11
  const requestId = requireId(input.requestId, "request-id");
12
12
  const snapshot = canonicalPath(input.snapshot, false);
@@ -21,7 +21,8 @@ export function prepareRunFork(input) {
21
21
  if (fs.existsSync(receiptFile)) {
22
22
  const receipt = readJson(receiptFile);
23
23
  const prior = receipt?.intent;
24
- if (!prior || prior.request_id !== requestId || prior.snapshot !== snapshot || prior.engine_version !== engineVersion || prior.integrity_checksum !== (input.integrityChecksum ?? null))
24
+ if (!prior || prior.request_id !== requestId || prior.snapshot !== snapshot || prior.engine_version !== engineVersion || prior.integrity_checksum !== (input.integrityChecksum ?? null)
25
+ || (prior.operational_decision_sha256 ?? null) !== (input.operationalDecisionFile ? fileHash(input.operationalDecisionFile) : null))
25
26
  throw new AppError("fork_request_conflict", "Fork output already belongs to another request", 1, { output });
26
27
  return { receipt, output };
27
28
  }
@@ -30,6 +31,7 @@ export function prepareRunFork(input) {
30
31
  if (manifest.schema_id !== "dd-flow/eval-run-snapshot@5" || manifest.purpose !== "stage_entry" || typeof manifest.stage_entry !== "string" || typeof manifest.run_id !== "string") {
31
32
  throw new AppError("fork_checkpoint_invalid", "Fork requires a sealed stage-entry snapshot", 2, { snapshot });
32
33
  }
34
+ const operational = prepareSnapshotOperationalDecision({ snapshot, runId: manifest.run_id, projectRoot: path.join(output, "project"), now: new Date().toISOString(), ...(input.operationalDecisionFile ? { file: input.operationalDecisionFile } : {}) });
33
35
  const intent = {
34
36
  schema_id: "dd-flow/run-fork-intent@1",
35
37
  request_id: requestId,
@@ -37,6 +39,7 @@ export function prepareRunFork(input) {
37
39
  snapshot_sha256: fileHash(manifestFile),
38
40
  engine_version: engineVersion,
39
41
  integrity_checksum: input.integrityChecksum ?? null,
42
+ ...(operational ? { operational_decision_sha256: crypto.createHash("sha256").update(operational.bytes).digest("hex") } : {}),
40
43
  target_stage: manifest.stage_entry
41
44
  };
42
45
  assertForkOutput(output, intentFile, intent);
@@ -58,6 +61,8 @@ export function forkRunFromSnapshot(context, input, prepared = prepareRunFork(in
58
61
  if ("receipt" in prepared)
59
62
  return prepared.receipt;
60
63
  const { manifest, snapshot, output, intent, intentFile, receiptFile, requestId } = prepared;
64
+ if (input.operationalDecisionFile && fileHash(input.operationalDecisionFile) !== intent.operational_decision_sha256)
65
+ throw new AppError("operational_decision_changed", "Accepted fork decision changed after preparation", 2);
61
66
  assertForkOutput(output, intentFile, intent);
62
67
  const projectRoot = path.join(output, "project");
63
68
  const home = path.join(output, "dd-flow-home");
@@ -69,7 +74,7 @@ export function forkRunFromSnapshot(context, input, prepared = prepareRunFork(in
69
74
  writeJsonAtomic(intentFile, { schema_id: "dd-flow/run-fork-intent@1", intent });
70
75
  fs.mkdirSync(projectRoot, { recursive: true, mode: 0o700 });
71
76
  const targetContext = createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_ENGINE_MODE: "1" }, "initialize", prearmedImportId ?? undefined);
72
- const restoreInput = { snapshot, projectRoot };
77
+ const restoreInput = { snapshot, projectRoot, ...(input.operationalDecisionFile ? { operationalDecisionFile: input.operationalDecisionFile } : {}) };
73
78
  const restored = restoreEvalRunSnapshot(targetContext, restoreInput, prepareEvalRunRestore(targetContext, restoreInput, manifest), prearmedImportId ?? undefined);
74
79
  const restoredContext = createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_ENGINE_MODE: "1" });
75
80
  try {