@deksden-com/dd-flow-cli 0.9.0-beta.121 → 0.9.0-beta.123

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 (49) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/hook-ingress.js +4 -3
  4. package/dist/cli/native-hook-ingress.js +34 -2
  5. package/dist/cli/run-cli.js +3 -0
  6. package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
  7. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +68 -6
  8. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +21 -5
  9. package/dist/harness-runtime/lib/dd-codex.mjs +5 -1
  10. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
  11. package/dist/harness-runtime/lib/dd-zcode.mjs +40 -16
  12. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  13. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  14. package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
  15. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  16. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  17. package/dist/harness-runtime/lib/managed-daemon.mjs +3 -2
  18. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  19. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  20. package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
  21. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  22. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  23. package/dist/runtime/context.js +1 -1
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/services/codex-capacity.js +131 -12
  26. package/dist/services/codex-hook-delivery.js +6 -2
  27. package/dist/services/eval-snapshots.js +13 -8
  28. package/dist/services/external-work-launch.js +27 -9
  29. package/dist/services/hooks.js +51 -59
  30. package/dist/services/lifecycle-command.js +1 -383
  31. package/dist/services/lifecycle-contract.js +1 -30
  32. package/dist/services/lifecycle-invocations.js +34 -13
  33. package/dist/services/merge-server.js +26 -9
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/run-controller-adapter.js +25 -3
  36. package/dist/services/run-controller-recovery.js +9 -2
  37. package/dist/services/run-controller.js +59 -20
  38. package/dist/services/run-observations.js +7 -2
  39. package/dist/services/runs.js +60 -47
  40. package/dist/services/stage-report-renderer.js +44 -2
  41. package/dist/services/verification-matrix.js +345 -0
  42. package/dist/services/vnext-code-review.js +30 -11
  43. package/dist/services/vnext-code.js +31 -10
  44. package/dist/services/vnext-merge.js +30 -7
  45. package/dist/services/vnext-plan-review.js +28 -7
  46. package/dist/services/vnext-plan.js +16 -3
  47. package/dist/services/work-registry.js +15 -1
  48. package/dist/storage/database.js +43 -9
  49. package/package.json +4 -1
@@ -777,7 +777,8 @@ export function issueLifecycleInvocation(context, command, scope) {
777
777
  }
778
778
  function commandWithInvocationId(command, id) {
779
779
  const at = shellSuffixOffset(command);
780
- return `${command.slice(0, at).trimEnd()} --invocation-id ${id} ${command.slice(at)}`.trimEnd();
780
+ const suffix = command.slice(at);
781
+ return `${command.slice(0, at)} --invocation-id ${id}${suffix ? /^\s/.test(suffix) ? suffix : ` ${suffix}` : ""}`;
781
782
  }
782
783
  /** The attempt owns authority; the caller owns presentation-only shell input.
783
784
  * Preserve current stdin/heredoc/response-file text while validating that it
@@ -909,11 +910,7 @@ export function observeLifecycleInvocation(context, input) {
909
910
  storage(context);
910
911
  const supplied = input.id ? context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [input.id]) : undefined;
911
912
  const analysis = parseLifecycleCommand(input.command);
912
- if (analysis.kind !== "standalone" || analysis.invocation.wrapped) {
913
- if (analysis.kind !== "none" && ["grok-acp", "antigravity-cli"].includes(input.harness ?? "") && input.projectId
914
- && managedNativeOwner(context, input.identity, input.harness, input.projectId)) {
915
- throw new AppError("invocation_command_invalid", "Managed native lifecycle call must be one direct standalone command", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
916
- }
913
+ if (analysis.kind === "none" || analysis.supported === false) {
917
914
  const eventKey = input.recordReceipt();
918
915
  if (analysis.kind !== "none") {
919
916
  const retained = context.db.get("SELECT outcome_json FROM hook_events WHERE event_key = ?", [eventKey]);
@@ -928,22 +925,32 @@ export function observeLifecycleInvocation(context, input) {
928
925
  ...outcome,
929
926
  lifecycle_observation: {
930
927
  code: "lifecycle_shell_syntax_invalid",
931
- reason: analysis.kind === "compound" ? analysis.reason : "wrapped_lifecycle_command",
928
+ reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command",
932
929
  command_fingerprint: fingerprint(analysis.invocation),
933
930
  effect: "no_effect",
934
931
  recoverable: true
935
932
  }
936
933
  }), eventKey]);
937
934
  }
935
+ if (analysis.kind !== "none")
936
+ throw new AppError("lifecycle_shell_syntax_invalid", "This shell form cannot bind exactly one lifecycle execution; use the exact issued lifecycle command without the unsupported shell context", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command", event_key: eventKey });
938
937
  return { eventKey, duplicate: false, invocationId: null };
939
938
  }
940
- const candidates = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed') ORDER BY rowid DESC")
939
+ // Admit the one parsed leaf, never execute or fingerprint the prefix/suffix.
940
+ const leafCommand = analysis.invocation.command;
941
+ if (input.executionCwd !== undefined && !path.isAbsolute(input.executionCwd))
942
+ throw new AppError("invocation_directory_mismatch", "Bound lifecycle execution cwd must be absolute", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
943
+ const deliveredIdentity = JSON.stringify([input.identity.daemonId, input.identity.rootSessionId, input.identity.sessionId, input.identity.parentSessionId, input.identity.toolCallId,
944
+ ...(input.executionCwd === undefined ? [] : [{ executionCwd: canonicalPath(input.executionCwd) }])]);
945
+ const candidates = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing','settled') ORDER BY rowid DESC")
941
946
  .filter(row => {
947
+ if (["executing", "settled"].includes(row.status) && row.identity_json !== deliveredIdentity)
948
+ return false;
942
949
  const scope = JSON.parse(row.scope_json);
943
950
  if (scope.daemonId !== input.identity.daemonId || scope.rootSessionId !== input.identity.rootSessionId)
944
951
  return false;
945
952
  try {
946
- return fingerprint(invocation(canonicalManagedCommand(context, input.command, scope))) === row.fingerprint;
953
+ return fingerprint(invocation(canonicalManagedCommand(context, leafCommand, scope))) === row.fingerprint;
947
954
  }
948
955
  catch {
949
956
  return false;
@@ -979,7 +986,7 @@ export function observeLifecycleInvocation(context, input) {
979
986
  const project = requireProjectByRoot(context, scope.projectRoot);
980
987
  const identity = input.identity;
981
988
  checkNativeIdentity(scope, identity, initial.id);
982
- const identityJson = JSON.stringify([identity.daemonId, identity.rootSessionId, identity.sessionId, identity.parentSessionId, identity.toolCallId]);
989
+ const identityJson = deliveredIdentity;
983
990
  return context.db.writeTransaction(() => {
984
991
  const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [initial.id]);
985
992
  if (row.status === "settled")
@@ -992,6 +999,9 @@ export function observeLifecycleInvocation(context, input) {
992
999
  if (row.identity_json === identityJson) {
993
1000
  return { eventKey: row.event_key, duplicate: true, ...resolved };
994
1001
  }
1002
+ if (row.identity_json && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(identityJson).slice(0, 5))) {
1003
+ throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1004
+ }
995
1005
  const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
996
1006
  // A native hook may execute from a provider worktree while the issued
997
1007
  // lifecycle scope is anchored to the stable project root. Project
@@ -1019,7 +1029,7 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
1019
1029
  return null;
1020
1030
  const signature = (value) => {
1021
1031
  const parsed = parseLifecycleCommand(value);
1022
- const candidate = parsed.kind === "standalone" && !parsed.invocation.wrapped ? parsed.invocation : continuationCommand(value);
1032
+ const candidate = parsed.kind !== "none" && parsed.supported ? parsed.invocation : continuationCommand(value);
1023
1033
  if (!candidate)
1024
1034
  return null;
1025
1035
  return JSON.stringify([candidate.operation, candidate.args.positional,
@@ -1080,8 +1090,9 @@ export function observedLifecycleInvocation(context, command, daemonId) {
1080
1090
  if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'"))
1081
1091
  return null;
1082
1092
  const analysis = parseLifecycleCommand(command);
1083
- if (analysis.kind !== "standalone" || analysis.invocation.wrapped)
1093
+ if (analysis.kind === "none" || analysis.supported === false)
1084
1094
  return null;
1095
+ const leafCommand = analysis.invocation.command;
1085
1096
  // `issued` is a rendezvous, not authority: the CLI still blocks in
1086
1097
  // awaitLifecycleInvocation until the native observer commits the receipt.
1087
1098
  const matches = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing') ORDER BY rowid DESC")
@@ -1090,7 +1101,7 @@ export function observedLifecycleInvocation(context, command, daemonId) {
1090
1101
  const scope = JSON.parse(row.scope_json);
1091
1102
  if (daemonId && scope.daemonId !== daemonId)
1092
1103
  return false;
1093
- return fingerprint(invocation(canonicalManagedCommand(context, command, scope))) === row.fingerprint;
1104
+ return fingerprint(invocation(canonicalManagedCommand(context, leafCommand, scope))) === row.fingerprint;
1094
1105
  }
1095
1106
  catch {
1096
1107
  return false;
@@ -1348,6 +1359,16 @@ export async function awaitLifecycleInvocation(context, input) {
1348
1359
  }
1349
1360
  input.assertCurrent(JSON.parse(row.scope_json));
1350
1361
  if (row.status === "observed") {
1362
+ if (row.identity_json) {
1363
+ // Receipt display text is bounded/redacted and may not be valid JSON.
1364
+ // Only the native admission's typed identity binds an execution route.
1365
+ const identity = JSON.parse(row.identity_json);
1366
+ const route = identity[5]?.executionCwd;
1367
+ if (identity.length > 5 && (typeof route !== "string" || !path.isAbsolute(route)))
1368
+ throw new AppError("invocation_directory_mismatch", "Bound lifecycle execution cwd is invalid", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1369
+ if (typeof route === "string" && canonicalPath(process.cwd()) !== canonicalPath(route))
1370
+ throw new AppError("invocation_directory_mismatch", "Reached CLI cwd differs from the bound shell execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false, expected: route, actual: process.cwd(), actual_provenance: "cli_process_cwd" });
1371
+ }
1351
1372
  const claimed = context.db.run("UPDATE lifecycle_invocations SET status = 'executing', updated_at = ? WHERE id = ? AND status = 'observed'", [context.now(), input.id]);
1352
1373
  if (claimed.changes === 1)
1353
1374
  return { eventKey: row.event_key, replay: false };
@@ -109,7 +109,7 @@ async function dispatch(context, input) {
109
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]); };
110
110
  retain();
111
111
  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);
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 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
113
  leaseHeartbeat.unref();
114
114
  try {
115
115
  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 +124,46 @@ async function dispatch(context, input) {
124
124
  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
125
  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
126
  let prompted;
127
+ const promptDeadline = Date.now() + 45 * 60_000;
127
128
  try {
128
129
  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
130
  }
130
131
  catch (error) {
131
132
  if (runtimeHarness(input.profile) !== "codex-desktop")
132
133
  throw error;
133
- const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id),
134
+ const capacityIntents = (retained.capacity_intents ?? []);
135
+ const resumed = await continueCodexCapacity({ error, sessionId, operationId: String(retained.prompt_operation_id), allowSettledDescendants: true,
136
+ deadline: promptDeadline,
137
+ 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.`,
138
+ retainedIntents: () => capacityIntents,
134
139
  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`),
140
+ 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
141
  assertCurrent: () => {
137
142
  assertRunMutationAllowed(context, input.request.project_id, input.request.run_id, generation);
138
143
  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
144
+ if (current?.status === "completed" || current?.status === "action_required")
145
+ return;
146
+ if (!current || !["dispatching", "active"].includes(current.status) || current.dispatch_owner !== input.serverId || current.dispatch_lease_token !== token
140
147
  || current.dispatch_lease_expires_at <= context.now() || current.target_workspace !== input.request.target_workspace)
141
148
  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);
149
+ if (!["created", "running", "paused", "completed"].includes(context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status ?? ""))
150
+ throw new AppError("capacity_continuation_blocked", "MERGE Work has failed or lost its binding", 1);
144
151
  },
145
- retainIntent: intent => { retained.capacity_intent = intent; retain(); },
152
+ decision: () => {
153
+ const status = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id])?.status;
154
+ const workStatus = context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status;
155
+ return status === "completed" ? "completed" : status === "action_required" || workStatus === "paused" ? "paused" : "continue";
156
+ },
157
+ retainIntent: intent => { capacityIntents.push(intent); retained.capacity_intents = capacityIntents; retained.capacity_intent = intent; retain(); },
146
158
  prompt: (id, text, ordinal) => {
147
159
  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);
160
+ if (fs.existsSync(file)) {
161
+ if (fs.readFileSync(file, "utf8") !== text)
162
+ throw new AppError("capacity_continuation_conflict", "Retained MERGE continuation prompt differs", 1);
163
+ }
164
+ else
165
+ fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
166
+ 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}/2`, id);
150
167
  } });
151
168
  if (!resumed)
152
169
  throw error;
@@ -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
  }
@@ -34,6 +34,8 @@ export function controllerHitlAdmission(context, record, input) {
34
34
  }
35
35
  if (retained.capacity_intent?.predecessor !== predecessorId || retained.capacity_intent.operation_id !== input.operationId || retained.capacity_intent.ordinal !== ordinal)
36
36
  return false;
37
+ 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)
38
+ return false;
37
39
  }
38
40
  const state = JSON.parse(row.state_json);
39
41
  const session = state.current_session === null ? undefined : state.sessions[state.current_session];
@@ -208,8 +210,18 @@ export async function controllerAdapter(context, row, manifest, session, args, a
208
210
  const promptSha256 = promptFile >= 0 ? digestBytes(fs.readFileSync(args[promptFile + 1])) : inlinePrompt >= 0 ? digestBytes(args[inlinePrompt + 1] ?? "") : undefined;
209
211
  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
212
  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 });
213
+ 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]);
214
+ if (existing?.input_hash && existing.input_hash !== digest(input))
215
+ throw new AppError("controller_operation_conflict", "Retained operation has different dispatch parameters", 1, { operation_id: operationId });
216
+ if (existing?.status === "completed" && existing.receipt_json) {
217
+ const receipt = JSON.parse(existing.receipt_json);
218
+ validateReceipt(receipt, session, args);
219
+ return receipt;
220
+ }
221
+ if (!existing) {
222
+ 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]);
223
+ appendEvent(context, row.controller_id, "operation_requested", { operation_id: operationId, operation: args.slice(0, 2), session_id: session.id || null });
224
+ }
213
225
  try {
214
226
  assertOwner?.();
215
227
  let receipt;
@@ -217,7 +229,17 @@ export async function controllerAdapter(context, row, manifest, session, args, a
217
229
  try {
218
230
  const timeoutMs = args[0] === "session" && args[1] === "prompt" ? null
219
231
  : 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 } : {}) });
232
+ if (existing) {
233
+ const observed = await recoverCompletedOperation(context, manifest, session, executable, args, operationId, signal);
234
+ if (!observed) {
235
+ 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 } };
236
+ throw new AppError(cause.code, cause.message, 1, cause.details);
237
+ }
238
+ receipt = observed;
239
+ recoveredOperation = true;
240
+ }
241
+ else
242
+ receipt = await runHarnessAdapter({ executable, args, env: controllerEnvironment(context, manifest, session), operationId, runtimeOwner: session.owner, timeoutMs, ...(signal ? { signal } : {}) });
221
243
  }
222
244
  catch (error) {
223
245
  const retained = await recoverCompletedOperation(context, manifest, session, executable, args, operationId, signal);
@@ -213,15 +213,21 @@ 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
+ },
220
224
  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`),
225
+ inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${dispatch.operation_id}:capacity-inspect`),
222
226
  assertCurrent: () => {
223
227
  assertRecoveryOwner();
224
228
  const guard = recoveryGuard(context, row.project_id, row.run_id);
229
+ if (guard?.status === "recovered")
230
+ return;
225
231
  const binding = context.db.get("SELECT status, daemon_id FROM run_recovery_bindings WHERE recovery_id = ?", [recovery.recovery_id]);
226
232
  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
233
  if (guard?.status !== "resuming" || binding?.status !== "prepared" || binding.daemon_id !== daemonId || ack?.status !== "accepted")
@@ -232,6 +238,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
232
238
  || current.process.lease_expires_at <= context.now() || metadata.run_generation !== row.generation || !processIsAlive(current.process))
233
239
  throw new AppError("recovery_capacity_owner_changed", "Replacement native owner changed during capacity wait", 1);
234
240
  },
241
+ decision: () => recoveryGuard(context, row.project_id, row.run_id)?.status === "recovered" ? "completed" : "continue",
235
242
  retainIntent: intent => retainRecoveryCapacityIntent(executionContext, { projectId: row.project_id, runId: row.run_id, recoveryId: recovery.recovery_id,
236
243
  controllerId: row.controller_id, generation: row.generation, daemonId, intent, prompt: continuationText }),
237
244
  prompt: (operationId, prompt) => sendAck(operationId, prompt) });
@@ -30,7 +30,7 @@ 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";
@@ -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,24 +560,45 @@ 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 = ? AND operation_id IN (?, ?)", [row.controller_id, row.generation, firstOperationId, `${firstOperationId}:capacity:1:session.prompt`])
565
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
557
566
  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`),
567
+ inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
559
568
  assertCurrent: () => {
560
569
  assertOwner();
570
+ },
571
+ decision: async () => {
572
+ if (kind === "child") {
573
+ const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
574
+ if (!target)
575
+ return "reconciliation_blocked";
576
+ const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
577
+ const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
578
+ if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
579
+ return "completed";
580
+ }
561
581
  const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
562
582
  const next = view.continuation;
583
+ if (next.kind === "blocked")
584
+ return "reconciliation_blocked";
585
+ if (next.kind === "paused" && next.stage !== state.stage)
586
+ return "paused";
587
+ if (next.kind === "terminal" || next.stage !== state.stage)
588
+ return "completed";
563
589
  if (hitl) {
564
590
  const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
565
591
  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
592
  const accepted = answer && JSON.parse(answer.input_json);
567
593
  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 });
594
+ if (next.kind !== "paused")
595
+ return "continue";
596
+ if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
597
+ return "paused";
573
598
  }
599
+ else if (next.kind === "paused")
600
+ return "paused";
601
+ return next.kind === "wait" ? "reconciliation_blocked" : "continue";
574
602
  },
575
603
  retainIntent: intent => context.db.writeTransaction(() => {
576
604
  const changed = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
@@ -579,7 +607,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
579
607
  appendEvent(context, row.controller_id, "capacity_continuation", intent);
580
608
  }),
581
609
  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.` } : {}) });
610
+ ...(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
611
  if (!resumed)
584
612
  throw error;
585
613
  receipt = resumed;
@@ -773,18 +801,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
773
801
  const prior = child && state.child_capacity?.[child.session_id];
774
802
  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
803
  if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
776
- && child.native_turn_items?.observed === true && child.native_turn_items.possible_effects === false
804
+ && child.native_turn_items?.observed === true && (child.native_turn_items.possible_effects === false || child.native_turn_items.pending === false)
777
805
  && 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)
806
+ && (!links.length || links[0].status === "running") && ((prior?.attempts ?? 0) < 2 || prior?.turn_id === turnId && prior.dispatched === false)) {
807
+ const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
808
+ if (prior?.turn_id === turnId && !prepared)
780
809
  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;
810
+ const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
811
+ const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
812
+ if (!Number.isFinite(wait) || wait > 30_000)
813
+ throw new AppError("capacity_child_retry_delay_exceeded", "Native child retry delay exceeds the bounded continuation window", 1, { child_session_id: child.session_id, delay_ms: wait });
814
+ const operationId = prepared?.operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
782
815
  state.child_capacity ??= {};
783
- state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt };
816
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
784
817
  saveState(context, row, state);
785
- const operationId = `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
786
818
  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);
819
+ await delay(wait, undefined, { signal: cancellation.signal });
788
820
  assertOwner();
789
821
  const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
790
822
  if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
@@ -792,13 +824,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
792
824
  const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
793
825
  const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
794
826
  const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
827
+ if (still?.status === "completed" && currentLinks.length <= 1 && currentLinks.every(link => link.status === "completed")) {
828
+ state.last_receipt = fresh;
829
+ saveState(context, row, state);
830
+ continue;
831
+ }
795
832
  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
833
+ || still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false && still.native_turn_items.pending !== false || currentLinks.length > 1
797
834
  || currentLinks.some(link => link.status !== "running"))
798
835
  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
836
  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.`;
837
+ 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
838
  await prompt(session, text, operationId, "child");
839
+ state.child_capacity[child.session_id].dispatched = true;
840
+ saveState(context, row, state);
802
841
  const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
803
842
  state.last_receipt = after;
804
843
  saveState(context, row, state);
@@ -829,7 +868,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
829
868
  const results = await Promise.allSettled(fanout.works.map(async (work) => {
830
869
  try {
831
870
  assertOwner();
832
- const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage });
871
+ const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
833
872
  if (fatal)
834
873
  throw fatal;
835
874
  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)));