@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
- import { AppError } from "../shared/errors.js";
2
+ import { AppError, errorRecord as structuredErrorRecord } from "../shared/errors.js";
3
3
  import { runtimeHarness } from "./execution-policy.js";
4
+ import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
4
5
  export function requireRun(context, projectId, runId) {
5
6
  const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, status, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
6
7
  if (!run?.run_root)
@@ -18,8 +19,66 @@ export function controllerReceipt(row) {
18
19
  const manifest = JSON.parse(row.manifest_json);
19
20
  return { ok: true, schema_id: "dd-flow/run-controller-receipt@1", controller: { controller_id: row.controller_id, request_id: row.request_id, project_id: row.project_id, run_id: row.run_id, generation: row.generation, runtime_budget: manifest.budget ?? null, status: row.status, process_id: row.process_id, owner_pid: row.owner_pid, state_dir: row.state_dir, stage: state.stage, sessions: state.sessions.map(session => ({ session_id: session.id, harness: runtimeHarness(session.profile), profile_id: session.profile.id, state_dir: session.state_dir, journal: session.journal, stopped: session.stopped === true })), error: row.last_error_json ? JSON.parse(row.last_error_json) : null } };
20
21
  }
22
+ /** Read-only causal diagnostics, not a settlement or capture authorization. */
23
+ export function terminalControllerFinalization(context, project, runId, runStatus) {
24
+ if (!["done", "failed", "cancelled"].includes(runStatus) || !context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'"))
25
+ return null;
26
+ const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, controller_id DESC LIMIT 1", [project.id, runId]);
27
+ if (!row?.last_error_json || !["recovery_required", "capture_failed"].includes(row.status))
28
+ return null;
29
+ const error = JSON.parse(row.last_error_json);
30
+ if (error.details?.phase !== "terminal_finalization" || error.details.project_root !== project.root || error.details.run_id !== runId || error.details.controller_id !== row.controller_id)
31
+ return null;
32
+ let owner = "unknown";
33
+ try {
34
+ const manifest = JSON.parse(row.manifest_json);
35
+ const records = managedProcessStatus({ ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: manifest.resource_home } });
36
+ const record = records.find(item => item.id === row.process_id);
37
+ if (record && record.kind === "run-controller" && record.owner_id === row.controller_id && record.project_id === project.id && record.run_id === runId
38
+ && record.lease_token === row.process_lease_token && record.pid === row.owner_pid && record.pid && record.pid_started_at
39
+ && managedProcessRuntimeHome(record, context) === context.ddFlowHome) {
40
+ if (processTreeIsAlive(record))
41
+ owner = "live";
42
+ else if (["stopped", "failed"].includes(record.state) && record.finished_at)
43
+ owner = "absent";
44
+ }
45
+ // Any other still-owned controller record makes absence unproven.
46
+ if (owner === "absent" && records.some(item => item.id !== row.process_id && item.kind === "run-controller" && item.project_id === project.id && item.run_id === runId
47
+ && managedProcessRuntimeHome(item, context) === context.ddFlowHome && (!item.pid_started_at || !["stopped", "failed"].includes(item.state) || processTreeIsAlive(item))))
48
+ owner = "unknown";
49
+ }
50
+ catch { /* Missing registry or unreadable identity is uncertainty, never death. */ }
51
+ const control = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controls'")
52
+ && context.db.get("SELECT 1 FROM run_controls WHERE project_id = ? AND run_id = ? AND status IN ('requested','settled','resuming')", [project.id, runId]);
53
+ const capture = context.db.get("SELECT 1 FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND status = 'completed' AND json_extract(input_json, '$.action') = 'boundary_capture' AND json_type(input_json, '$.stage_entry') = 'null'", [row.controller_id, row.generation]);
54
+ return { schema_id: "dd-flow/terminal-finalization@1", issuer: "dd-flow", phase: "terminal_finalization", project_root: project.root, run_id: runId,
55
+ controller_id: row.controller_id, generation: row.generation, controller_status: row.status, run_status: runStatus,
56
+ status: !control && owner === "absent" ? "blocked" : "pending", owner, capture: capture ? "recorded" : "missing", error };
57
+ }
21
58
  export function appendEvent(context, id, type, data) { context.db.run("INSERT INTO run_controller_events (controller_id, event_type, data_json, created_at) VALUES (?, ?, ?, ?)", [id, type, JSON.stringify(data), context.now()]); }
22
- export function saveState(context, row, state) { context.db.run("UPDATE run_controllers SET state_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [JSON.stringify(state), context.now(), row.controller_id, row.owner_token]); }
59
+ export function saveState(context, row, state) {
60
+ const changed = context.db.run("UPDATE run_controllers SET state_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND generation = ? AND status NOT IN ('completed','cancelled','superseded')", [JSON.stringify(state), context.now(), row.controller_id, row.owner_token, row.generation]);
61
+ if (changed.changes !== 1)
62
+ throw new AppError("controller_owner_mismatch", "Controller state owner or generation changed", 1);
63
+ }
64
+ export function retainControllerCapacityIntent(context, row, state, intent, sessionId, kind, assertCurrent) {
65
+ const root = intent.root_operation_id;
66
+ if (!root || intent.session_id !== sessionId || !intent.refusal)
67
+ throw new AppError("capacity_continuation_conflict", "Capacity intent has no exact controller root/Session/refusal", 1);
68
+ const pending = { root_operation_id: root, predecessor: intent.predecessor, successor: intent.operation_id, session_id: sessionId, generation: row.generation, kind };
69
+ context.db.writeTransaction(() => {
70
+ assertCurrent();
71
+ const owner = context.db.get("SELECT status FROM run_controllers WHERE controller_id = ? AND owner_token = ? AND generation = ?", [row.controller_id, row.owner_token, row.generation]);
72
+ if (!owner || !["running", "waiting_for_user", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(owner.status))
73
+ throw new AppError("controller_owner_mismatch", "Controller no longer owns continuation preparation", 1);
74
+ 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_root: root, capacity_failure: intent.refusal, capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
75
+ if (changed.changes !== 1)
76
+ throw new AppError("capacity_continuation_blocked", "Failed native operation changed before continuation", 1);
77
+ saveState(context, row, { ...state, pending_capacity: pending });
78
+ appendEvent(context, row.controller_id, "capacity_continuation", intent);
79
+ });
80
+ state.pending_capacity = pending;
81
+ }
23
82
  export function retainControllerContinuation(state, fingerprint, error) {
24
83
  if (state.last_continuation === fingerprint)
25
84
  throw new AppError(error.code, error.message, 1, error.details);
@@ -28,9 +87,11 @@ export function retainControllerContinuation(state, fingerprint, error) {
28
87
  export function digest(value) { return digestBytes(JSON.stringify(value)); }
29
88
  export function digestBytes(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
30
89
  export function errorRecord(error) {
31
- return {
32
- code: error instanceof AppError ? error.code : "controller_failed",
33
- message: error instanceof Error ? error.message : String(error),
34
- ...(error instanceof AppError && Object.keys(error.details).length ? { details: error.details } : {})
35
- };
90
+ const record = structuredErrorRecord(error);
91
+ let typed = false;
92
+ try {
93
+ typed = typeof error?.code === "string";
94
+ }
95
+ catch { /* Preserve a serializable controller failure for unreadable diagnostics. */ }
96
+ return { ...record, code: typed ? record.code : "controller_failed" };
36
97
  }
@@ -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
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,7 +582,7 @@ 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,
585
+ const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
564
586
  retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
565
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),
566
588
  retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
@@ -605,14 +627,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
605
627
  }
606
628
  else if (next.kind === "paused")
607
629
  return "paused";
608
- 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";
609
631
  },
610
- retainIntent: intent => context.db.writeTransaction(() => {
611
- 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_root: firstOperationId, capacity_failure: intent.refusal, capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
612
- if (changed.changes !== 1)
613
- throw new AppError("capacity_continuation_blocked", "Failed native operation changed before continuation", 1);
614
- appendEvent(context, row.controller_id, "capacity_continuation", intent);
615
- }),
632
+ retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
616
633
  prompt: (id, value) => send(value, id),
617
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.` } : {}) });
618
635
  if (!resumed)
@@ -627,10 +644,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
627
644
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
628
645
  throw error;
629
646
  await stopAfterFailure(error);
630
- cancellation.abort();
647
+ if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
648
+ cancellation.abort();
631
649
  throw fatal ?? error;
632
650
  }
633
651
  state.last_receipt = receipt;
652
+ delete state.pending_capacity;
634
653
  saveState(context, row, state);
635
654
  return receipt;
636
655
  };
@@ -649,14 +668,37 @@ async function executeController(context, row, manifest, state, assertOwnership,
649
668
  for (;;) {
650
669
  // Renewal touches the resource registry; never do it from a callback that
651
670
  // may already own the RUN writer.
652
- renewLease();
671
+ await renewLease();
653
672
  assertOwner();
654
673
  if (state.current_session !== null && !state.sessions[state.current_session].stopped)
655
674
  bindInvocationScope(state.sessions[state.current_session]);
656
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
+ }
657
688
  const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
658
689
  const next = view.continuation;
690
+ terminalFinalization = next.kind === "terminal";
659
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
+ }
660
702
  if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
661
703
  const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
662
704
  if (key !== lastCapture) {
@@ -664,12 +706,6 @@ async function executeController(context, row, manifest, state, assertOwnership,
664
706
  lastCapture = key;
665
707
  }
666
708
  }
667
- if (completed && manifest.request.stop_after === state.stage) {
668
- await settle();
669
- setStatus("stop_target_reached");
670
- appendEvent(context, row.controller_id, "stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
671
- return;
672
- }
673
709
  if (next.kind === "terminal") {
674
710
  await settle();
675
711
  setStatus(run.status === "cancelled" ? "cancelled" : "completed");
@@ -691,6 +727,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
691
727
  const input = JSON.parse(answer.input_json);
692
728
  if (digestBytes(fs.readFileSync(input.file)) !== input.sha256)
693
729
  throw new AppError("context_checksum_mismatch", "Accepted HITL answer bytes changed", 1);
730
+ await renewLease();
694
731
  assertOwner();
695
732
  // The accepted answer is logical intent; only its generation-bound
696
733
  // adapter operation represents a physical dispatch that stop must drain.
@@ -745,6 +782,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
745
782
  if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
746
783
  if (session)
747
784
  await stopSession(session);
785
+ await renewLease();
748
786
  assertOwner();
749
787
  const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
750
788
  fs.mkdirSync(directory, { mode: 0o700 });
@@ -782,6 +820,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
782
820
  // A CODE_REVIEW fork has source files but intentionally no installed
783
821
  // dependencies. Its repair Work executes checks just like CODE Work.
784
822
  const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
823
+ await renewLease();
785
824
  assertOwner();
786
825
  appendEvent(context, row.controller_id, "code_workspace_prepared", { stage: next.stage, readiness_status: readiness.status ?? null });
787
826
  }
@@ -802,6 +841,25 @@ async function executeController(context, row, manifest, state, assertOwnership,
802
841
  saveState(context, row, state);
803
842
  children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
804
843
  const failed = children.filter(child => child.status === "failed");
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
+ }
805
863
  // Continue one existing child at a time; another failed child does not
806
864
  // create a second fanout wave or share this child's refusal counter.
807
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;
@@ -815,7 +873,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
815
873
  && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
816
874
  && (!links.length || links[0].status === "running")) {
817
875
  const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
818
- if (prior?.turn_id === turnId && !prepared)
876
+ if (prior?.turn_id === turnId && prior.dispatched === true && !prepared)
819
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 });
820
878
  const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
821
879
  if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
@@ -838,21 +896,24 @@ async function executeController(context, row, manifest, state, assertOwnership,
838
896
  const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
839
897
  state.child_capacity ??= {};
840
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 };
841
- saveState(context, row, state);
842
- appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
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 }); });
843
900
  const until = performance.now() + wait;
844
901
  while (performance.now() < until) {
845
902
  await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
903
+ await renewLease();
846
904
  assertOwner();
847
905
  }
906
+ await renewLease();
848
907
  assertOwner();
849
908
  const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
850
- 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))
851
912
  throw new AppError("capacity_child_followup_blocked", "RUN changed during native child capacity wait", 1, { child_session_id: child.session_id });
852
913
  const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
853
914
  const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
854
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 })]);
855
- 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)) {
856
917
  state.last_receipt = fresh;
857
918
  saveState(context, row, state);
858
919
  continue;
@@ -861,6 +922,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
861
922
  || !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
862
923
  || currentLinks.some(link => link.status !== "running"))
863
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();
864
926
  assertOwner();
865
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.`;
866
928
  await prompt(session, text, operationId, "child");
@@ -887,7 +949,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
887
949
  if (fanout.kind === "wait") {
888
950
  setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
889
951
  await delay(1000);
890
- 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);
891
953
  saveState(context, row, state);
892
954
  continue;
893
955
  }
@@ -895,6 +957,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
895
957
  if (fanout.kind === "external") {
896
958
  const results = await Promise.allSettled(fanout.works.map(async (work) => {
897
959
  try {
960
+ await renewLease();
898
961
  assertOwner();
899
962
  const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
900
963
  if (fatal)
@@ -932,8 +995,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
932
995
  catch (error) {
933
996
  if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
934
997
  throw error;
935
- await stopAfterFailure(fatal ?? error);
936
- 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;
937
1009
  }
938
1010
  finally {
939
1011
  clearInterval(monitor);