@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
  12. package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
  21. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  22. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  23. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/schemas/plan-review-decision.schema.json +3 -3
  26. package/dist/services/code-checks.js +14 -18
  27. package/dist/services/codex-capacity.js +251 -31
  28. package/dist/services/controller-fanout.js +1 -0
  29. package/dist/services/eval-snapshots.js +13 -8
  30. package/dist/services/external-work-launch.js +36 -11
  31. package/dist/services/harness-adapter.js +21 -15
  32. package/dist/services/managed-processes.js +14 -6
  33. package/dist/services/merge-server.js +35 -12
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/review-deferral.js +23 -0
  36. package/dist/services/run-controller-adapter.js +33 -8
  37. package/dist/services/run-controller-recovery.js +26 -2
  38. package/dist/services/run-controller.js +91 -24
  39. package/dist/services/run-observations.js +7 -2
  40. package/dist/services/run-recovery-runtime.js +49 -3
  41. package/dist/services/runs.js +1 -1
  42. package/dist/services/stage-report-renderer.js +25 -2
  43. package/dist/services/verification-matrix.js +57 -8
  44. package/dist/services/vnext-code-review.js +22 -8
  45. package/dist/services/vnext-merge.js +20 -2
  46. package/dist/services/vnext-plan-review.js +87 -19
  47. package/dist/services/work-registry.js +11 -2
  48. 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 = 'dispatching' 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
+ 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 resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id),
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: () => adapterCall(["session", "inspect", "--session-id", sessionId, ...common], 30_000, undefined, `${input.request.merge_request_id}:${token}:capacity-inspect`),
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 !== "dispatching" || current.dispatch_owner !== input.serverId || current.dispatch_lease_token !== token
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 !== "created")
143
- throw new AppError("capacity_continuation_blocked", "MERGE Work already changed state", 1);
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.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
149
- return adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], 45 * 60 * 1000, `MERGE ${input.request.merge_request_id} Session ${sessionId} capacity continuation ${ordinal}/2`, id);
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 false;
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 = /^(.*):capacity:([12]):session\.prompt$/.exec(input.operationId);
19
- const baseId = successor ? successor[1] : input.operationId;
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 = Number(successor[2]);
24
- const predecessorId = ordinal === 1 ? baseId : `${baseId}:capacity:1:session.prompt`;
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.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]);
212
- appendEvent(context, row.controller_id, "operation_requested", { operation_id: operationId, operation: args.slice(0, 2), session_id: session.id || null });
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
- receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs, ...(signal ? { signal } : {}) });
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 = `The preceding native recovery ACK Turn failed before any observed tool or file effect. This is 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}`;
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: () => inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${dispatch.operation_id}:capacity-inspect`),
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/dd-codex.mjs";
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, `${value}\n`, { flag: "wx", mode: 0o600 });
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: () => inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
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" || next.stage !== state.stage || !accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
569
- throw new AppError("capacity_continuation_blocked", "Accepted HITL pause changed during capacity wait", 1, { continuation: next });
570
- }
571
- else if (next.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(next.kind)) {
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: `The preceding Turn failed before any tool call. Continue the same accepted HITL answer in this Session. If the pause is still current, run this exact previously issued standalone command once without changing the answer:\n\n${resumeCommand}\n\nDo not accept or deliver the answer again if the pause has changed.` } : {}) });
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
- const child = failed.length === 1 && children.every(item => item === failed[0] || item.status === "completed") ? failed[0] : undefined;
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?.observed === true && child.native_turn_items.possible_effects === false
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") && (prior?.attempts ?? 0) < 2) {
779
- if (prior?.turn_id === turnId)
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
- await delay(attempt === 1 ? 5_000 : 15_000);
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?.observed !== true || still.native_turn_items.possible_effects !== false || currentLinks.length > 1
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 effect-free failed 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.`;
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
- if (!session.transcript_path || covered().has(JSON.stringify({ harness_id: session.harness, session_id: nativeId })))
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)));