@deksden-com/dd-flow-cli 0.9.0-beta.122 → 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.
@@ -1,14 +1,114 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
+ import crypto from "node:crypto";
2
3
  import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
3
- /** Only a terminal, effect-free native Turn may get another productive Turn. */
4
+ import { AppError } from "../shared/errors.js";
5
+ /** Permit acquisition must still belong to the original task, not merely an
6
+ * open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
7
+ export function assertCapacityOwnerAdmission(context, record, operationId) {
8
+ if (!operationId || !record.project_id || !record.run_id)
9
+ return;
10
+ const root = operationId.replace(/:capacity:[12]:session\.prompt$/, "");
11
+ if (root === operationId && !/:child:[a-f0-9]+:followup:[12]$/.test(root))
12
+ return;
13
+ const reject = () => { throw new AppError("capacity_owner_boundary_changed", "Queued continuation no longer belongs to an unfinished task; reconcile its durable state", 1, { operation_id: operationId, work_id: record.work_id, run_id: record.run_id }); };
14
+ if (context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_recovery_bindings'")) {
15
+ if (context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.status = 'recovered' AND b.status = 'accepted' AND json_extract(b.runtime_dispatch_json, '$.operation_id') = ?", [record.project_id, record.run_id, root]))
16
+ reject();
17
+ }
18
+ if (record.work_id) {
19
+ const work = context.db.get("SELECT status FROM works WHERE work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
20
+ const merge = context.db.get("SELECT status FROM merge_requests WHERE executor_work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
21
+ if (merge && !["dispatching", "active"].includes(merge.status))
22
+ reject();
23
+ // MERGE owns the Stage/request boundary: finishing its Work does not
24
+ // necessarily commit stage finish yet. External workers own only Work.
25
+ const mergeOwner = JSON.parse(record.metadata_json).role === "merge";
26
+ if (!work || !(merge && mergeOwner ? ["created", "running", "completed"] : ["created", "running"]).includes(work.status))
27
+ reject();
28
+ }
29
+ if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'"))
30
+ return;
31
+ const controller = context.db.get("SELECT state_json FROM run_controllers WHERE controller_id = ? AND project_id = ? AND run_id = ?", [record.owner_id, record.project_id, record.run_id]);
32
+ if (!controller)
33
+ return;
34
+ const state = JSON.parse(controller.state_json);
35
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
36
+ const index = JSON.parse(run?.index_json ?? "{}");
37
+ if (state.stage && (index.current_stage && index.current_stage !== state.stage || index.stage_runs?.some(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status))))
38
+ reject();
39
+ const child = Object.entries(state.child_capacity ?? {}).find(([, intent]) => intent.operation_id === root)?.[0];
40
+ if (child && context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND s.provider_session_id = ? AND w.status <> 'running'", [record.project_id, record.run_id, child]))
41
+ reject();
42
+ }
43
+ export function capacityDelay(error, ordinal, now = Date.now()) {
44
+ const native = error?.details?.provider_error;
45
+ const headers = native?.headers;
46
+ const header = headers && Object.entries(headers).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
47
+ const value = header ?? native?.retryAfter ?? native?.retry_after;
48
+ const seconds = typeof value === "number" || typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) * 1000 : typeof value === "string" ? Date.parse(value) - now : NaN;
49
+ return Math.max(ordinal === 1 ? 5_000 : 15_000, Number.isFinite(seconds) ? Math.max(0, seconds) : 0);
50
+ }
51
+ /** Continue a confirmed terminal Turn, never replay its completed effects. */
4
52
  export async function continueCodexCapacity(input) {
5
53
  let { error, operationId } = input;
6
- for (let ordinal = 1; ordinal <= 2; ordinal += 1) {
54
+ const history = input.retainedIntents?.() ?? [];
55
+ const rootId = input.operationId.replace(/:capacity:[12]:session\.prompt$/, "");
56
+ const text = input.continuationText ?? "Continue the same assigned task in this Session. Check current durable RUN, Work and Stage state first. Never repeat an observed lifecycle command, accepted HITL answer, check or merge apply; use only an exact issued successor. Stop at the assigned boundary.";
57
+ const promptSha = crypto.createHash("sha256").update(text).digest("hex");
58
+ let ordinal = Number(/:capacity:([12]):session\.prompt$/.exec(operationId)?.[1] ?? 0) + 1;
59
+ // Reconcile a retained dispatch before preparing another. Native absence is
60
+ // the only safe way to reuse a prepared intent; a lost reply is not absence.
61
+ for (const intent of history) {
62
+ if (intent.predecessor !== operationId || intent.ordinal !== ordinal || intent.operation_id !== `${rootId}:capacity:${ordinal}:session.prompt`)
63
+ continue;
64
+ if (intent.prompt_sha256 !== promptSha || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)))
65
+ throw error;
66
+ let observed;
67
+ try {
68
+ observed = await input.inspectOperation(intent.operation_id);
69
+ }
70
+ catch (cause) {
71
+ let code = cause;
72
+ for (let depth = 0; depth < 4 && code?.code === "harness_adapter_failed"; depth++)
73
+ code = code.details?.cause;
74
+ if (code?.code !== "operation_not_found")
75
+ throw error;
76
+ break;
77
+ }
78
+ if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt" || observed.primary_error)
79
+ throw error;
80
+ if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
81
+ input.signal?.throwIfAborted();
82
+ input.assertCurrent();
83
+ return observed.result;
84
+ }
85
+ if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
86
+ throw error;
87
+ operationId = intent.operation_id;
88
+ error = observed.error;
89
+ ordinal += 1;
90
+ }
91
+ const decide = async () => {
92
+ input.signal?.throwIfAborted();
93
+ input.assertCurrent();
94
+ const decision = await input.decision?.() ?? "continue";
95
+ input.signal?.throwIfAborted();
96
+ input.assertCurrent();
97
+ if (decision === "reconciliation_blocked")
98
+ throw error;
99
+ if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
100
+ throw error;
101
+ return decision === "continue" ? null : { provider_session_id: input.sessionId, capacity_decision: decision };
102
+ };
103
+ for (; ordinal <= 2; ordinal += 1) {
7
104
  if (!terminalCodexOverload(error, input.sessionId)) {
8
105
  if (ordinal === 1)
9
106
  return null;
10
107
  throw error;
11
108
  }
109
+ const completed = await decide();
110
+ if (completed)
111
+ return completed;
12
112
  let prior;
13
113
  try {
14
114
  prior = await input.inspectOperation(operationId);
@@ -21,14 +121,16 @@ export async function continueCodexCapacity(input) {
21
121
  const failure = prior.error;
22
122
  const turnId = failure?.details?.turn_id;
23
123
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
24
- || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)
25
- || failure.details?.native_turn_items?.observed !== true || failure.details.native_turn_items.possible_effects !== false) {
124
+ || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
26
125
  if (ordinal === 1)
27
126
  return null;
28
127
  throw error;
29
128
  }
30
129
  const settled = async () => {
31
- const seen = await input.inspectSession();
130
+ const seen = await input.inspectSession(turnId);
131
+ const items = seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : failure.details?.native_turn_items;
132
+ if (items?.observed !== true || typeof items.possible_effects !== "boolean" || items.pending === true || items.possible_effects === true && items.pending !== false)
133
+ return false;
32
134
  const settlement = seen.settlement;
33
135
  const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
34
136
  const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
@@ -38,9 +140,8 @@ export async function continueCodexCapacity(input) {
38
140
  && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
39
141
  : ids.length === 1 && ids[0] === input.sessionId;
40
142
  return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
41
- && (!settlement || ["settled", "not_required"].includes(settlement.state ?? ""));
143
+ && (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
42
144
  };
43
- input.assertCurrent();
44
145
  try {
45
146
  if (!await settled()) {
46
147
  if (ordinal === 1)
@@ -53,16 +154,34 @@ export async function continueCodexCapacity(input) {
53
154
  return null;
54
155
  throw error;
55
156
  }
56
- const nextId = `${input.operationId}:capacity:${ordinal}:session.prompt`;
57
- input.retainIntent({ predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, not_before: new Date(Date.now() + (ordinal === 1 ? 5_000 : 15_000)).toISOString() });
58
- await delay(ordinal === 1 ? 5_000 : 15_000);
59
- input.assertCurrent();
157
+ const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
158
+ const existing = history.find(item => item.operation_id === nextId);
159
+ if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
160
+ throw error;
161
+ const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
162
+ if (!Number.isFinite(wait) || wait > 30_000 || input.deadline !== undefined && Date.now() + wait >= input.deadline)
163
+ throw error;
164
+ if (!existing)
165
+ input.retainIntent({ predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString() });
166
+ await delay(wait, undefined, { signal: input.signal });
167
+ const afterWait = await decide();
168
+ if (afterWait)
169
+ return afterWait;
60
170
  if (!await settled().catch(() => false))
61
171
  throw error;
172
+ const beforeDispatch = await decide();
173
+ if (beforeDispatch)
174
+ return beforeDispatch;
62
175
  try {
63
- return await input.prompt(nextId, input.continuationText ?? "Continue the same assigned task in this Session. Check current durable RUN, Work and Stage state first. Never repeat an observed lifecycle command, accepted HITL answer, check or merge apply; use only an exact issued successor. Stop at the assigned boundary.", ordinal);
176
+ return await input.prompt(nextId, text, ordinal);
64
177
  }
65
178
  catch (nextError) {
179
+ // A task can complete/pause while the daemon queues for its permit.
180
+ // Admission then refuses the stale prompt; accept that owner decision
181
+ // before treating the transport's rejection as another provider failure.
182
+ const finished = await decide();
183
+ if (finished)
184
+ return finished;
66
185
  error = nextError;
67
186
  operationId = nextId;
68
187
  }
@@ -409,7 +409,12 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
409
409
  let result, primary, failed = false;
410
410
  try {
411
411
  const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
412
+ const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
412
413
  rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
414
+ const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
415
+ if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
416
+ throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
417
+ restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId }), restored.now()]);
413
418
  registerProject(restored, { root: projectRoot });
414
419
  requireProjectByRoot(restored, projectRoot);
415
420
  if (selectedProfile && routingInput && runtimeLineage) {
@@ -423,14 +428,6 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
423
428
  fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
424
429
  importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
425
430
  }
426
- const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
427
- if (!recovery)
428
- assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
429
- else
430
- restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
431
- if (!status.run.run_root)
432
- throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
433
- const runHome = status.run.run_root;
434
431
  const relocation = recovery ? {
435
432
  schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
436
433
  path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
@@ -440,6 +437,14 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
440
437
  fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
441
438
  writeJson(relocationFile, relocation);
442
439
  }
440
+ const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
441
+ if (!recovery)
442
+ assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
443
+ else
444
+ restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
445
+ if (!status.run.run_root)
446
+ throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
447
+ const runHome = status.run.run_root;
443
448
  const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
444
449
  if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
445
450
  throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
@@ -93,7 +93,7 @@ export async function launchExternalWork(context, input) {
93
93
  const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
94
94
  const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
95
95
  try {
96
- const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
96
+ const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence });
97
97
  await call(["daemon", "start", ...common]);
98
98
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
99
99
  const created = await call(["session", "create", ...common], runtimeHarness(resolved.worker) === "zcode-acp" ? 180_000 : undefined);
@@ -111,6 +111,7 @@ export async function launchExternalWork(context, input) {
111
111
  const promptFile = path.join(root, "launch.md");
112
112
  fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
113
113
  const promptOperationId = `${operationId}:session.prompt`;
114
+ const promptDeadline = Date.now() + 45 * 60_000;
114
115
  let prompted;
115
116
  try {
116
117
  prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], 45 * 60_000);
@@ -118,37 +119,54 @@ export async function launchExternalWork(context, input) {
118
119
  catch (error) {
119
120
  if (runtimeHarness(resolved.worker) !== "codex-desktop")
120
121
  throw error;
121
- const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId,
122
+ const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
123
+ const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
124
+ const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
125
+ ...(input.signal ? { signal: input.signal } : {}), deadline: promptDeadline,
126
+ continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
127
+ retainedIntents: () => capacityIntents,
122
128
  inspectOperation: id => call(["daemon", "operation", "--state-dir", root, "--operation-id", id, "--json"], 30_000, `${id}:capacity-observe`),
123
- inspectSession: () => call(["session", "inspect", "--session-id", sessionId, ...common], 30_000, `${operationId}:capacity-inspect`),
129
+ inspectSession: turnId => call(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, `${operationId}:capacity-inspect`),
124
130
  assertCurrent: () => {
125
131
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
126
132
  const current = context.db.get("SELECT w.status, l.status AS launch_status FROM works w JOIN work_launches l ON l.work_id = w.work_id WHERE w.work_id = ? AND l.operation_id = ?", [work.work_id, operationId]);
127
- if (current?.status !== "created" || current.launch_status !== "launching")
133
+ if (!current || !["created", "running", "paused", "completed"].includes(current.status) || current.launch_status !== "launching")
128
134
  throw new AppError("capacity_continuation_blocked", "External Work changed before its native continuation", 1, { work_id: work.work_id, state: current });
129
135
  if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
130
136
  throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
131
137
  },
138
+ decision: () => {
139
+ const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
140
+ return status === "completed" ? "completed" : status === "paused" ? "paused" : "continue";
141
+ },
132
142
  retainIntent: intent => {
133
- const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, capacity_intent: intent }), context.now(), operationId]);
143
+ capacityIntents.push(intent);
144
+ const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, capacity_intent: intent, capacity_intents: capacityIntents }), context.now(), operationId]);
134
145
  if (changed.changes !== 1)
135
146
  throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
136
147
  },
137
148
  prompt: (id, text, ordinal) => {
138
149
  const file = path.join(root, `capacity-${ordinal}.md`);
139
- fs.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
140
- return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], 45 * 60_000, id);
150
+ if (fs.existsSync(file)) {
151
+ if (fs.readFileSync(file, "utf8") !== text)
152
+ throw new AppError("capacity_continuation_conflict", "Retained Work continuation prompt differs", 1);
153
+ }
154
+ else
155
+ fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
156
+ return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], Math.max(1, promptDeadline - Date.now()), id);
141
157
  } });
142
158
  if (!resumed)
143
159
  throw error;
144
160
  prompted = resumed;
145
161
  }
146
162
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
147
- context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created, prompt: prompted }), context.now(), operationId]);
163
+ const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
164
+ context.db.run("UPDATE work_launches SET status = 'start_requested', provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), create: created, prompt: prompted }), context.now(), operationId]);
148
165
  return { ok: true, work_id: work.work_id, operation_id: operationId, status: "start_requested", session: { harness_id: runtimeHarness(resolved.worker), session_id: sessionId }, profile_hash: resolved.profile_hash, journal_path: journal };
149
166
  }
150
167
  catch (error) {
151
- context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ error: errorRecord(error) }), context.now(), operationId]);
168
+ const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
169
+ context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(error) }), context.now(), operationId]);
152
170
  throw error;
153
171
  }
154
172
  }
@@ -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) });