@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.
- package/CHANGELOG.md +8 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +52 -6
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +21 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +5 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +15 -4
- package/dist/harness-runtime/lib/managed-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/services/codex-capacity.js +131 -12
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +27 -9
- package/dist/services/merge-server.js +26 -9
- package/dist/services/review-copy.js +21 -0
- package/dist/services/run-controller-adapter.js +25 -3
- package/dist/services/run-controller-recovery.js +9 -2
- package/dist/services/run-controller.js +59 -20
- package/dist/services/run-observations.js +7 -2
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +27 -7
- package/dist/services/vnext-code-review.js +6 -4
- package/dist/services/vnext-plan-review.js +7 -2
- package/dist/services/work-registry.js +2 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 = `${
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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.
|
|
140
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
143
|
-
throw new AppError("capacity_continuation_blocked", "MERGE Work
|
|
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
|
-
|
|
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.
|
|
149
|
-
|
|
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.
|
|
212
|
-
|
|
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
|
-
|
|
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 = `
|
|
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:
|
|
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) });
|