@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.
- package/CHANGELOG.md +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- 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,
|
|
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/
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
535
|
-
|
|
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 = ?
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
&&
|
|
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")
|
|
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 >
|
|
813
|
-
throw new AppError("
|
|
814
|
-
const
|
|
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
|
-
|
|
819
|
-
|
|
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"
|
|
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 (
|
|
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
|
|
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,
|
|
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
|
-
|
|
908
|
-
|
|
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 {
|