@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124
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 +18 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/domain/stage-catalog.js +1 -1
- 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-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +251 -31
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +36 -11
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +35 -12
- package/dist/services/review-copy.js +21 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +33 -8
- package/dist/services/run-controller-recovery.js +26 -2
- package/dist/services/run-controller.js +91 -24
- package/dist/services/run-observations.js +7 -2
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +57 -8
- package/dist/services/vnext-code-review.js +22 -8
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +87 -19
- package/dist/services/work-registry.js +11 -2
- package/package.json +1 -1
|
@@ -106,10 +106,11 @@ async function dispatch(context, input) {
|
|
|
106
106
|
const executable = resolveHarnessCommand(context.ddFlowHome, input.profile.harness, "adapter");
|
|
107
107
|
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", stateDir, "--journal", adapterJournal, "--cwd", input.request.target_workspace, "--provider", input.profile.provider, "--model", input.profile.model, "--reasoning", input.profile.reasoning, "--mode", input.profile.mode, "--permission", input.profile.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, input.profile.harness), "--json"];
|
|
108
108
|
const retained = { profile: input.profile, executable, owner: runtimeOwner, session_id: null, state_dir: stateDir, create_operation_id: `${input.request.merge_request_id}:${token}:session.create`, prompt_operation_id: `${input.request.merge_request_id}:${token}:session.prompt` };
|
|
109
|
-
const retain = () => { context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]);
|
|
109
|
+
const retain = () => { const changed = context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]); if (changed.changes !== 1)
|
|
110
|
+
throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner changed before durable receipt retention", 1); };
|
|
110
111
|
retain();
|
|
111
112
|
const leaseHeartbeat = setInterval(() => { if (guarded(context, input.request) || (recoveryGuard(context, input.request.project_id, input.request.run_id)?.generation ?? 0) !== generation)
|
|
112
|
-
return; const now = context.now(); context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status
|
|
113
|
+
return; const now = context.now(); context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status IN ('dispatching','active') AND dispatch_owner = ? AND dispatch_lease_token = ?", [new Date(Date.now() + 120_000).toISOString(), now, input.request.merge_request_id, input.serverId, token]); }, 30_000);
|
|
113
114
|
leaseHeartbeat.unref();
|
|
114
115
|
try {
|
|
115
116
|
const adapterCall = (args, timeoutMs, progressMessage, id = `${input.request.merge_request_id}:${token}:${args[0]}.${args[1]}`) => { assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation); return runHarnessAdapter({ executable, args, env: runtimeContext.env, runtimeOwner, operationId: id, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), ...(progressMessage ? { progressMessage } : {}), onEvidence: (value) => event(input.journal, { type: "adapter_call", ...value }) }); };
|
|
@@ -124,29 +125,51 @@ async function dispatch(context, input) {
|
|
|
124
125
|
const stageCommand = managedLifecycleCommand(runtimeContext, `${flowCommand(runtimeContext)} stage start ${input.request.run_id} --stage merge --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`);
|
|
125
126
|
fs.writeFileSync(promptFile, `Start the assigned MERGE stage now. Your first tool call must be this exact standalone command:\n\n${stageCommand}\n\nTrust and follow the complete stage packet it returns. Continue through merge apply and stage finish. Stop only when the stage reaches a terminal result or explicitly requests user/operator action.\n\n${stageLifecycleInstruction}\n`);
|
|
126
127
|
let prompted;
|
|
128
|
+
const promptDeadline = Date.now() + 45 * 60_000;
|
|
129
|
+
retained.prompt_deadline = promptDeadline;
|
|
130
|
+
retain();
|
|
127
131
|
try {
|
|
128
132
|
prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60 * 1000, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
|
|
129
133
|
}
|
|
130
134
|
catch (error) {
|
|
131
135
|
if (runtimeHarness(input.profile) !== "codex-desktop")
|
|
132
136
|
throw error;
|
|
133
|
-
const
|
|
137
|
+
const capacityIntents = (retained.capacity_intents ?? []);
|
|
138
|
+
const capacityFailures = (retained.capacity_failures ?? []);
|
|
139
|
+
const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
|
|
140
|
+
deadline: promptDeadline,
|
|
141
|
+
continuationText: `Continue only MERGE ${input.request.merge_request_id}, Work ${input.request.executor_work_id}, RUN ${input.request.run_id} in this same Session and target ${input.request.target_workspace}. First inspect current durable Stage/Work/MERGE receipts. Never repeat Stage start, accepted answer, completed apply or checks. Follow only the current exact issued successor and stop at the original boundary.`,
|
|
142
|
+
retainedIntents: () => capacityIntents,
|
|
143
|
+
retainedFailures: () => capacityFailures,
|
|
144
|
+
retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retained.capacity_failures = capacityFailures; retain(); },
|
|
134
145
|
inspectOperation: id => adapterCall(["daemon", "operation", "--state-dir", stateDir, "--operation-id", id, "--json"], 30_000, undefined, `${id}:capacity-observe`),
|
|
135
|
-
inspectSession:
|
|
146
|
+
inspectSession: turnId => adapterCall(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, undefined, `${input.request.merge_request_id}:${token}:capacity-inspect`),
|
|
136
147
|
assertCurrent: () => {
|
|
137
148
|
assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation);
|
|
138
149
|
const current = context.db.get("SELECT status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, target_workspace FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id]);
|
|
139
|
-
if (current?.status
|
|
150
|
+
if (current?.status === "completed" || current?.status === "action_required")
|
|
151
|
+
return;
|
|
152
|
+
if (!current || !["dispatching", "active"].includes(current.status) || current.dispatch_owner !== input.serverId || current.dispatch_lease_token !== token
|
|
140
153
|
|| current.dispatch_lease_expires_at <= context.now() || current.target_workspace !== input.request.target_workspace)
|
|
141
154
|
throw new AppError("capacity_continuation_blocked", "MERGE dispatch owner or target changed during capacity wait", 1, { request_id: input.request.merge_request_id });
|
|
142
|
-
if (context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status
|
|
143
|
-
throw new AppError("capacity_continuation_blocked", "MERGE Work
|
|
155
|
+
if (!["created", "running", "paused", "completed"].includes(context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status ?? ""))
|
|
156
|
+
throw new AppError("capacity_continuation_blocked", "MERGE Work has failed or lost its binding", 1);
|
|
157
|
+
},
|
|
158
|
+
decision: () => {
|
|
159
|
+
const status = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id])?.status;
|
|
160
|
+
const workStatus = context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status;
|
|
161
|
+
return status === "completed" ? "completed" : status === "action_required" || workStatus === "paused" ? "paused" : "continue";
|
|
144
162
|
},
|
|
145
|
-
retainIntent: intent => { retained.capacity_intent = intent; retain(); },
|
|
163
|
+
retainIntent: intent => { capacityIntents.push(intent); retained.capacity_intents = capacityIntents; retained.capacity_intent = intent; retain(); },
|
|
146
164
|
prompt: (id, text, ordinal) => {
|
|
147
165
|
const file = path.join(stateDir, `capacity-${ordinal}.md`);
|
|
148
|
-
fs.
|
|
149
|
-
|
|
166
|
+
if (fs.existsSync(file)) {
|
|
167
|
+
if (fs.readFileSync(file, "utf8") !== text)
|
|
168
|
+
throw new AppError("capacity_continuation_conflict", "Retained MERGE continuation prompt differs", 1);
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
172
|
+
return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], Math.max(1, promptDeadline - Date.now()), `MERGE ${input.request.merge_request_id} Session ${sessionId} capacity continuation ${ordinal}`, id);
|
|
150
173
|
} });
|
|
151
174
|
if (!resumed)
|
|
152
175
|
throw error;
|
|
@@ -193,6 +216,6 @@ function processAlive(pid) { if (!pid)
|
|
|
193
216
|
process.kill(pid, 0);
|
|
194
217
|
return true;
|
|
195
218
|
}
|
|
196
|
-
catch {
|
|
197
|
-
return
|
|
219
|
+
catch (error) {
|
|
220
|
+
return error.code !== "ESRCH";
|
|
198
221
|
} }
|
|
@@ -5,6 +5,27 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { ignoredWorkspacePaths } from "./code-checks.js";
|
|
7
7
|
import { localEnvironmentAllowed, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
|
|
8
|
+
export function reviewerSourceInputs(root, aspectIds) {
|
|
9
|
+
const criteria = [...new Set(aspectIds)].sort().map(id => {
|
|
10
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(id))
|
|
11
|
+
throw new AppError("review_criterion_invalid", "Assigned aspect has an invalid identity", 1, { aspect_id: id });
|
|
12
|
+
return `.memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects/${id}.md`;
|
|
13
|
+
});
|
|
14
|
+
const orientation = [".memory-bank/index.md", ".memory-bank/spec/engineering/index.md", ".memory-bank/spec/engineering/coding-standards.md"].filter(relative => fs.existsSync(path.join(root, relative)));
|
|
15
|
+
return [...criteria, ...orientation].map(relative => {
|
|
16
|
+
const file = path.join(root, relative);
|
|
17
|
+
if (!fs.existsSync(file))
|
|
18
|
+
throw new AppError("review_criterion_missing", "Assigned reviewer criterion is missing", 1, { path: relative });
|
|
19
|
+
const inside = path.relative(fs.realpathSync(root), fs.realpathSync(file));
|
|
20
|
+
if (inside === ".." || inside.startsWith(`..${path.sep}`) || !fs.statSync(file).isFile())
|
|
21
|
+
throw new AppError("review_criterion_invalid", "Reviewer source must be a file in its workspace", 1, { path: relative });
|
|
22
|
+
return { path: relative, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function reviewerSourceInstruction(root, aspectIds) {
|
|
26
|
+
const inputs = reviewerSourceInputs(root, aspectIds);
|
|
27
|
+
return ["Read the assigned canonical criterion files and the available project orientation below before independent review. Apply each criterion's scope; a small behavior change is not by itself a reason to omit testing or evidence. These starting inputs do not limit relevant project investigation. Do not edit them.", ...inputs.map(input => `- ${path.join(root, input.path)} (SHA256 ${input.sha256})`)].join("\n");
|
|
28
|
+
}
|
|
8
29
|
export function reviewEnvironmentAllowed(file) {
|
|
9
30
|
return localEnvironmentAllowed(file);
|
|
10
31
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
6
|
+
/** A deferral names one project-local file, never an arbitrary path. */
|
|
7
|
+
export function reviewDefEvidence(workspaceRoot, defId) {
|
|
8
|
+
try {
|
|
9
|
+
if (!defId || !/^DEF-[A-Za-z0-9][A-Za-z0-9_-]*$/.test(defId))
|
|
10
|
+
throw new Error("invalid DEF identifier");
|
|
11
|
+
const relative = `.memory-bank/defs/${defId}.md`;
|
|
12
|
+
const file = assertPortableArtifactRef(relative, { workspaceRoot, runHome: workspaceRoot, runId: "" });
|
|
13
|
+
const directory = fs.realpathSync(path.join(workspaceRoot, ".memory-bank", "defs"));
|
|
14
|
+
const root = fs.realpathSync(workspaceRoot);
|
|
15
|
+
const directoryRelative = path.relative(root, directory);
|
|
16
|
+
if (directoryRelative === ".." || directoryRelative.startsWith(`..${path.sep}`) || path.isAbsolute(directoryRelative) || path.dirname(file) !== directory || !fs.statSync(file).isFile())
|
|
17
|
+
throw new Error("DEF must be a regular file in the project defs directory");
|
|
18
|
+
return { def_id: defId, path: relative, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new AppError("deferral_invalid", "A deferral requires a readable project-local named DEF", 2, { def_id: defId ?? null, cause: error instanceof Error ? error.message : String(error) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -5,6 +5,7 @@ import { AppError } from "../shared/errors.js";
|
|
|
5
5
|
import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
|
|
6
6
|
import { runtimeHarness } from "./execution-policy.js";
|
|
7
7
|
import { appendEvent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
8
|
+
import { CAPACITY_POLICY, parseCapacityOperation } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
8
9
|
/** A paused RUN admits only its controller's exact accepted answer Turn. */
|
|
9
10
|
export function controllerHitlAdmission(context, record, input) {
|
|
10
11
|
if (input.operation !== "session.prompt" || !input.operationId || !input.promptSha256)
|
|
@@ -15,13 +16,13 @@ export function controllerHitlAdmission(context, record, input) {
|
|
|
15
16
|
if (!row || row.generation !== JSON.parse(record.metadata_json).run_generation)
|
|
16
17
|
return false;
|
|
17
18
|
const suffix = `:prompt:${row.generation}`;
|
|
18
|
-
const successor =
|
|
19
|
-
const baseId = successor
|
|
19
|
+
const successor = parseCapacityOperation(input.operationId);
|
|
20
|
+
const baseId = successor.root;
|
|
20
21
|
if (!baseId.endsWith(suffix))
|
|
21
22
|
return false;
|
|
22
|
-
if (successor) {
|
|
23
|
-
const ordinal =
|
|
24
|
-
const predecessorId = ordinal === 1 ? baseId : `${baseId}:capacity
|
|
23
|
+
if (successor.ordinal > 0) {
|
|
24
|
+
const ordinal = successor.ordinal;
|
|
25
|
+
const predecessorId = ordinal === 1 ? baseId : `${baseId}:capacity:${ordinal - 1}:session.prompt`;
|
|
25
26
|
const predecessor = context.db.get("SELECT status, receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [predecessorId, row.controller_id, row.generation]);
|
|
26
27
|
if (predecessor?.status !== "failed" || !predecessor.receipt_json)
|
|
27
28
|
return false;
|
|
@@ -34,6 +35,10 @@ export function controllerHitlAdmission(context, record, input) {
|
|
|
34
35
|
}
|
|
35
36
|
if (retained.capacity_intent?.predecessor !== predecessorId || retained.capacity_intent.operation_id !== input.operationId || retained.capacity_intent.ordinal !== ordinal)
|
|
36
37
|
return false;
|
|
38
|
+
if (retained.capacity_intent.policy !== CAPACITY_POLICY)
|
|
39
|
+
return false;
|
|
40
|
+
if (!retained.capacity_intent.not_before || !Number.isFinite(Date.parse(retained.capacity_intent.not_before)) || Date.parse(retained.capacity_intent.not_before) > Date.parse(context.now()) || retained.capacity_intent.prompt_sha256 !== input.promptSha256)
|
|
41
|
+
return false;
|
|
37
42
|
}
|
|
38
43
|
const state = JSON.parse(row.state_json);
|
|
39
44
|
const session = state.current_session === null ? undefined : state.sessions[state.current_session];
|
|
@@ -208,8 +213,18 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
208
213
|
const promptSha256 = promptFile >= 0 ? digestBytes(fs.readFileSync(args[promptFile + 1])) : inlinePrompt >= 0 ? digestBytes(args[inlinePrompt + 1] ?? "") : undefined;
|
|
209
214
|
const input = { executable, args, owner: session.owner.owner_id, state_dir: session.state_dir, ...(args[0] === "session" && args[1] === "prompt" && promptSha256 ? { prompt_sha256: promptSha256 } : {}) };
|
|
210
215
|
const now = context.now();
|
|
211
|
-
context.db.
|
|
212
|
-
|
|
216
|
+
const existing = context.db.get("SELECT input_hash,status,receipt_json,error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [operationId, row.controller_id, row.generation]);
|
|
217
|
+
if (existing?.input_hash && existing.input_hash !== digest(input))
|
|
218
|
+
throw new AppError("controller_operation_conflict", "Retained operation has different dispatch parameters", 1, { operation_id: operationId });
|
|
219
|
+
if (existing?.status === "completed" && existing.receipt_json) {
|
|
220
|
+
const receipt = JSON.parse(existing.receipt_json);
|
|
221
|
+
validateReceipt(receipt, session, args);
|
|
222
|
+
return receipt;
|
|
223
|
+
}
|
|
224
|
+
if (!existing) {
|
|
225
|
+
context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'dispatching', ?, ?)", [operationId, row.controller_id, row.generation, digest(input), JSON.stringify(input), now, now]);
|
|
226
|
+
appendEvent(context, row.controller_id, "operation_requested", { operation_id: operationId, operation: args.slice(0, 2), session_id: session.id || null });
|
|
227
|
+
}
|
|
213
228
|
try {
|
|
214
229
|
assertOwner?.();
|
|
215
230
|
let receipt;
|
|
@@ -217,7 +232,17 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
217
232
|
try {
|
|
218
233
|
const timeoutMs = args[0] === "session" && args[1] === "prompt" ? null
|
|
219
234
|
: args[0] === "session" && args[1] === "create" && runtimeHarness(session.profile) === "zcode-acp" ? 180_000 : 120_000;
|
|
220
|
-
|
|
235
|
+
if (existing) {
|
|
236
|
+
const observed = await recoverCompletedOperation(context, manifest, session, executable, args, operationId, signal);
|
|
237
|
+
if (!observed) {
|
|
238
|
+
const cause = existing.error_json ? JSON.parse(existing.error_json) : { code: "controller_operation_outcome_unknown", message: "Retained dispatch has no observed terminal result", details: { operation_id: operationId } };
|
|
239
|
+
throw new AppError(cause.code, cause.message, 1, cause.details);
|
|
240
|
+
}
|
|
241
|
+
receipt = observed;
|
|
242
|
+
recoveredOperation = true;
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs, ...(signal ? { signal } : {}) });
|
|
221
246
|
}
|
|
222
247
|
catch (error) {
|
|
223
248
|
const retained = await recoverCompletedOperation(context, manifest, session, executable, args, operationId, signal);
|
|
@@ -213,15 +213,38 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
213
213
|
catch (error) {
|
|
214
214
|
if (runtimeHarness(session.profile) !== "codex-desktop")
|
|
215
215
|
throw error;
|
|
216
|
-
const continuationText = `
|
|
216
|
+
const continuationText = `Continue the same retained Session and recovery generation. First inspect the current RUN recovery state; only if the exact ACK still awaits acceptance, execute the exact standalone acceptance command below once. Never deliver it again after acceptance. Then stop at the original Stage or Work-graph boundary.\n\n${dispatch.prompt}`;
|
|
217
217
|
const inspect = (args, operationId) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
|
|
218
218
|
args, env: controllerEnvironment(executionContext, manifest, session), runtimeOwner: session.owner, operationId, timeoutMs: 30_000 });
|
|
219
219
|
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: dispatch.operation_id, allowSettledDescendants: true, continuationText,
|
|
220
|
+
retainedIntents: () => {
|
|
221
|
+
const binding = context.db.get("SELECT runtime_dispatch_json FROM run_recovery_bindings WHERE recovery_id = ?", [recovery.recovery_id]);
|
|
222
|
+
return binding?.runtime_dispatch_json ? JSON.parse(binding.runtime_dispatch_json).capacity_intents ?? [] : [];
|
|
223
|
+
},
|
|
224
|
+
retainedFailures: () => {
|
|
225
|
+
const binding = context.db.get("SELECT runtime_dispatch_json FROM run_recovery_bindings WHERE recovery_id = ?", [recovery.recovery_id]);
|
|
226
|
+
return binding?.runtime_dispatch_json ? JSON.parse(binding.runtime_dispatch_json).capacity_failures ?? [] : [];
|
|
227
|
+
},
|
|
228
|
+
retainFailure: (_id, refusal) => {
|
|
229
|
+
assertRecoveryOwner();
|
|
230
|
+
context.db.writeTransaction(() => {
|
|
231
|
+
const binding = context.db.get("SELECT runtime_dispatch_json FROM run_recovery_bindings WHERE recovery_id = ? AND daemon_id = ? AND status = 'prepared'", [recovery.recovery_id, daemonId]);
|
|
232
|
+
if (!binding)
|
|
233
|
+
throw new AppError("recovery_capacity_owner_changed", "Recovery binding changed before refusal retention", 1);
|
|
234
|
+
const retained = JSON.parse(binding.runtime_dispatch_json);
|
|
235
|
+
retained.capacity_failures = [...(retained.capacity_failures ?? []), refusal];
|
|
236
|
+
const changed = context.db.run("UPDATE run_recovery_bindings SET runtime_dispatch_json = ? WHERE recovery_id = ? AND daemon_id = ? AND status = 'prepared' AND runtime_dispatch_json = ?", [JSON.stringify(retained), recovery.recovery_id, daemonId, binding.runtime_dispatch_json]);
|
|
237
|
+
if (changed.changes !== 1)
|
|
238
|
+
throw new AppError("recovery_capacity_owner_changed", "Recovery binding changed during refusal retention", 1);
|
|
239
|
+
});
|
|
240
|
+
},
|
|
220
241
|
inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
|
|
221
|
-
inspectSession:
|
|
242
|
+
inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${dispatch.operation_id}:capacity-inspect`),
|
|
222
243
|
assertCurrent: () => {
|
|
223
244
|
assertRecoveryOwner();
|
|
224
245
|
const guard = recoveryGuard(context, row.project_id, row.run_id);
|
|
246
|
+
if (guard?.status === "recovered")
|
|
247
|
+
return;
|
|
225
248
|
const binding = context.db.get("SELECT status, daemon_id FROM run_recovery_bindings WHERE recovery_id = ?", [recovery.recovery_id]);
|
|
226
249
|
const ack = context.db.get("SELECT status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [recovery.operation_id, row.controller_id, row.generation]);
|
|
227
250
|
if (guard?.status !== "resuming" || binding?.status !== "prepared" || binding.daemon_id !== daemonId || ack?.status !== "accepted")
|
|
@@ -232,6 +255,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
232
255
|
|| current.process.lease_expires_at <= context.now() || metadata.run_generation !== row.generation || !processIsAlive(current.process))
|
|
233
256
|
throw new AppError("recovery_capacity_owner_changed", "Replacement native owner changed during capacity wait", 1);
|
|
234
257
|
},
|
|
258
|
+
decision: () => recoveryGuard(context, row.project_id, row.run_id)?.status === "recovered" ? "completed" : "continue",
|
|
235
259
|
retainIntent: intent => retainRecoveryCapacityIntent(executionContext, { projectId: row.project_id, runId: row.run_id, recoveryId: recovery.recovery_id,
|
|
236
260
|
controllerId: row.controller_id, generation: row.generation, daemonId, intent, prompt: continuationText }),
|
|
237
261
|
prompt: (operationId, prompt) => sendAck(operationId, prompt) });
|
|
@@ -30,12 +30,12 @@ import { requireRun, requireController, controllerReceipt, appendEvent, saveStat
|
|
|
30
30
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
31
31
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady } from "./run-controller-process.js";
|
|
32
32
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
33
|
-
import { continueCodexCapacity } from "./codex-capacity.js";
|
|
33
|
+
import { capacityDelay, continueCodexCapacity } from "./codex-capacity.js";
|
|
34
34
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
35
35
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
36
36
|
import { requestRunControl } from "./run-control-worker.js";
|
|
37
37
|
import { storageSessionId } from "./session-identity.js";
|
|
38
|
-
import { terminalCodexOverload } from "../harness-runtime/lib/
|
|
38
|
+
import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
39
39
|
export { resumeRunController } from "./run-controller-recovery.js";
|
|
40
40
|
export function prepareRunControllerLaunch(context, input) {
|
|
41
41
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
|
|
@@ -534,8 +534,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
534
534
|
await reconcilePromptPredecessor(session);
|
|
535
535
|
const firstOperationId = operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
536
536
|
const send = (value, id) => {
|
|
537
|
+
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
|
+
if (retained) {
|
|
539
|
+
const dispatch = JSON.parse(retained.input_json);
|
|
540
|
+
if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
|
|
541
|
+
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);
|
|
543
|
+
}
|
|
537
544
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
538
|
-
fs.writeFileSync(file,
|
|
545
|
+
fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
|
|
539
546
|
state.turns += 1;
|
|
540
547
|
saveState(context, row, state);
|
|
541
548
|
return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal);
|
|
@@ -553,33 +560,61 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
553
560
|
throw error;
|
|
554
561
|
const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
|
|
555
562
|
args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
|
|
556
|
-
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId,
|
|
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 = ?", [row.controller_id, row.generation])
|
|
565
|
+
.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
|
+
retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
|
|
567
|
+
.flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_root === firstOperationId && retained.capacity_failure ? [retained.capacity_failure] : []; }),
|
|
568
|
+
retainFailure: (id, refusal) => {
|
|
569
|
+
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]);
|
|
570
|
+
if (changed.changes !== 1)
|
|
571
|
+
throw new AppError("capacity_continuation_blocked", "Failed Turn owner changed before its refusal was retained", 1);
|
|
572
|
+
},
|
|
557
573
|
inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
|
|
558
|
-
inspectSession:
|
|
574
|
+
inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
|
|
559
575
|
assertCurrent: () => {
|
|
560
576
|
assertOwner();
|
|
577
|
+
},
|
|
578
|
+
decision: async () => {
|
|
579
|
+
if (kind === "child") {
|
|
580
|
+
const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
|
|
581
|
+
if (!target)
|
|
582
|
+
return "reconciliation_blocked";
|
|
583
|
+
const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
|
|
584
|
+
const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
|
|
585
|
+
if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
|
|
586
|
+
return "completed";
|
|
587
|
+
}
|
|
561
588
|
const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
562
589
|
const next = view.continuation;
|
|
590
|
+
if (next.kind === "blocked")
|
|
591
|
+
return "reconciliation_blocked";
|
|
592
|
+
if (next.kind === "paused" && next.stage !== state.stage)
|
|
593
|
+
return "paused";
|
|
594
|
+
if (next.kind === "terminal" || next.stage !== state.stage)
|
|
595
|
+
return "completed";
|
|
563
596
|
if (hitl) {
|
|
564
597
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
565
598
|
const answer = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status = 'accepted'", [answerId, row.controller_id]);
|
|
566
599
|
const accepted = answer && JSON.parse(answer.input_json);
|
|
567
600
|
const pause = view.index.stage_runs.find(item => item.stage === accepted?.stage)?.pause;
|
|
568
|
-
if (next.kind !== "paused"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
throw new AppError("capacity_continuation_blocked", "RUN changed while native capacity was unavailable", 1, { continuation: next });
|
|
601
|
+
if (next.kind !== "paused")
|
|
602
|
+
return "continue";
|
|
603
|
+
if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
|
|
604
|
+
return "paused";
|
|
573
605
|
}
|
|
606
|
+
else if (next.kind === "paused")
|
|
607
|
+
return "paused";
|
|
608
|
+
return next.kind === "wait" ? "reconciliation_blocked" : "continue";
|
|
574
609
|
},
|
|
575
610
|
retainIntent: intent => context.db.writeTransaction(() => {
|
|
576
|
-
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]);
|
|
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]);
|
|
577
612
|
if (changed.changes !== 1)
|
|
578
613
|
throw new AppError("capacity_continuation_blocked", "Failed native operation changed before continuation", 1);
|
|
579
614
|
appendEvent(context, row.controller_id, "capacity_continuation", intent);
|
|
580
615
|
}),
|
|
581
616
|
prompt: (id, value) => send(value, id),
|
|
582
|
-
...(resumeCommand ? { continuationText: `
|
|
617
|
+
...(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.` } : {}) });
|
|
583
618
|
if (!resumed)
|
|
584
619
|
throw error;
|
|
585
620
|
receipt = resumed;
|
|
@@ -740,6 +775,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
740
775
|
}
|
|
741
776
|
state.native_baseline = controllerNativeChildren(state.last_receipt ?? {}, session.id).map(child => child.session_id);
|
|
742
777
|
state.stage = next.stage;
|
|
778
|
+
state.child_capacity = {};
|
|
743
779
|
saveState(context, row, state);
|
|
744
780
|
setStatus("running");
|
|
745
781
|
if (next.stage === "code" || next.stage === "code-review") {
|
|
@@ -766,25 +802,49 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
766
802
|
saveState(context, row, state);
|
|
767
803
|
children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
|
|
768
804
|
const failed = children.filter(child => child.status === "failed");
|
|
769
|
-
|
|
805
|
+
// Continue one existing child at a time; another failed child does not
|
|
806
|
+
// create a second fanout wave or share this child's refusal counter.
|
|
807
|
+
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;
|
|
770
808
|
const turnId = child?.terminal_turn_id;
|
|
771
809
|
const native = child && turnId ? { code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: turnId, native_turn_id: turnId,
|
|
772
|
-
terminal_status: "failed", provider_error: child.provider_error } } : null;
|
|
810
|
+
terminal_status: "failed", provider_error: child.provider_error, observed_at: child.terminal_observed_at } } : null;
|
|
773
811
|
const prior = child && state.child_capacity?.[child.session_id];
|
|
774
812
|
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 })]) : [];
|
|
775
813
|
if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
|
|
776
|
-
&& child.native_turn_items
|
|
814
|
+
&& nativeItemsSettled(child.native_turn_items)
|
|
777
815
|
&& inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
|
|
778
|
-
&& (!links.length || links[0].status === "running")
|
|
779
|
-
|
|
816
|
+
&& (!links.length || links[0].status === "running")) {
|
|
817
|
+
const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
|
|
818
|
+
if (prior?.turn_id === turnId && !prepared)
|
|
780
819
|
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 });
|
|
781
|
-
const attempt = (prior?.attempts ?? 0) + 1;
|
|
820
|
+
const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
|
|
821
|
+
if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
|
|
822
|
+
throw new AppError("capacity_continuation_conflict", "Native child continuation policy or ordinal is incompatible", 1);
|
|
823
|
+
const refusals = [...(prior?.refusals ?? [])];
|
|
824
|
+
if (!refusals.some(item => item.turn_id === turnId)) {
|
|
825
|
+
const refusal = refusalObservation(native, attempt - 1, child.terminal_observed_at ?? context.now());
|
|
826
|
+
refusals.push(refusal);
|
|
827
|
+
state.child_capacity ??= {};
|
|
828
|
+
state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
|
|
829
|
+
saveState(context, row, state);
|
|
830
|
+
}
|
|
831
|
+
const burst = overloadBurst(refusals);
|
|
832
|
+
if (burst)
|
|
833
|
+
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 });
|
|
834
|
+
const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
|
|
835
|
+
if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
|
|
836
|
+
throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
|
|
837
|
+
const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
|
|
838
|
+
const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
|
|
782
839
|
state.child_capacity ??= {};
|
|
783
|
-
state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt };
|
|
840
|
+
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 };
|
|
784
841
|
saveState(context, row, state);
|
|
785
|
-
const operationId = `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
|
|
786
842
|
appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
|
|
787
|
-
|
|
843
|
+
const until = performance.now() + wait;
|
|
844
|
+
while (performance.now() < until) {
|
|
845
|
+
await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
|
|
846
|
+
assertOwner();
|
|
847
|
+
}
|
|
788
848
|
assertOwner();
|
|
789
849
|
const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
790
850
|
if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
|
|
@@ -792,13 +852,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
792
852
|
const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
793
853
|
const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
|
|
794
854
|
const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
|
|
855
|
+
if (still?.status === "completed" && currentLinks.length <= 1 && currentLinks.every(link => link.status === "completed")) {
|
|
856
|
+
state.last_receipt = fresh;
|
|
857
|
+
saveState(context, row, state);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
795
860
|
if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
|
|
796
|
-
|| still.native_turn_items
|
|
861
|
+
|| !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
|
|
797
862
|
|| currentLinks.some(link => link.status !== "running"))
|
|
798
863
|
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 });
|
|
799
864
|
assertOwner();
|
|
800
|
-
const text = `The existing native child ${child.session_id} has a confirmed
|
|
865
|
+
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.`;
|
|
801
866
|
await prompt(session, text, operationId, "child");
|
|
867
|
+
state.child_capacity[child.session_id].dispatched = true;
|
|
868
|
+
saveState(context, row, state);
|
|
802
869
|
const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
803
870
|
state.last_receipt = after;
|
|
804
871
|
saveState(context, row, state);
|
|
@@ -829,7 +896,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
829
896
|
const results = await Promise.allSettled(fanout.works.map(async (work) => {
|
|
830
897
|
try {
|
|
831
898
|
assertOwner();
|
|
832
|
-
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage });
|
|
899
|
+
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
|
|
833
900
|
if (fatal)
|
|
834
901
|
throw fatal;
|
|
835
902
|
appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
|
|
@@ -90,7 +90,7 @@ export function runObservations(context, projectId, runId) {
|
|
|
90
90
|
const actual = fs.realpathSync(file);
|
|
91
91
|
if (!inside(home, actual))
|
|
92
92
|
throw new Error("journal escaped owned home");
|
|
93
|
-
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
93
|
+
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
94
94
|
if (!fs.fstatSync(fd).isFile())
|
|
95
95
|
throw new Error("journal is not a regular file");
|
|
96
96
|
replayToolJournal(file, { harness: source.session.harness_id, rootSessionId: sessionId, text: fs.readFileSync(fd, "utf8"), ...(source.session.harness_id === "codex-desktop" ? { expectedSessionIds: expectedCodexIds } : {}) }, reducer);
|
|
@@ -114,7 +114,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
114
114
|
// Select transcript fallback only after replay reveals an actual gap.
|
|
115
115
|
for (const session of sessions) {
|
|
116
116
|
const nativeId = session.provider_session_id ?? session.session_id;
|
|
117
|
-
|
|
117
|
+
const sessionSummary = reducer.summary(nativeId);
|
|
118
|
+
if (!session.transcript_path || covered().has(JSON.stringify({ harness_id: session.harness, session_id: nativeId }))
|
|
119
|
+
&& sessionSummary.completeness === "complete" && sessionSummary.outcome_completeness === "complete")
|
|
118
120
|
continue;
|
|
119
121
|
const id = `transcript:${session.harness}:${nativeId}`;
|
|
120
122
|
add(id, nativeId, session.transcript_path, home, "current", session.harness);
|
|
@@ -136,6 +138,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
136
138
|
if (!expectedIds.has(JSON.stringify(event.session))) {
|
|
137
139
|
reducer.turns.delete(key);
|
|
138
140
|
}
|
|
141
|
+
for (const [key, event] of reducer.sources)
|
|
142
|
+
if (!expectedIds.has(JSON.stringify(event.session)))
|
|
143
|
+
reducer.sources.delete(key);
|
|
139
144
|
const summary = reducer.summary();
|
|
140
145
|
const observed = new Set(summary.sessions.map(s => JSON.stringify(s.session)));
|
|
141
146
|
const missing = expected.filter(s => !observed.has(JSON.stringify(s)));
|