@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.
- package/CHANGELOG.md +16 -0
- package/README.md +10 -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/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/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- 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 +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- 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/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- 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 +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- 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 +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- 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/vnext-code-review.js +10 -9
- 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 +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- 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 +34 -4
- 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
|
@@ -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) {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,
|
|
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
|
|
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,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.
|
|
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
|
-
|
|
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"
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
936
|
-
|
|
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);
|