@deksden-com/dd-flow-cli 0.9.0-beta.39 → 0.9.0-beta.40
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 +12 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +3 -1
- package/dist/services/eval-snapshots.js +36 -4
- package/dist/services/hooks.js +5 -5
- package/dist/services/run-controller-adapter.js +37 -2
- package/dist/services/run-controller.js +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.40
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2b4b714: Admit ordinary managed HITL answers through their exact generation-bound native
|
|
8
|
+
prompt, preserving pause, Session, raw-answer and prompt integrity. Keep accepted
|
|
9
|
+
answer intent separate from physical delivery so stop drains native operations
|
|
10
|
+
and recovery can deliver an unanswered pause without replaying completed work.
|
|
11
|
+
- c5eeb6f: Capture owned Codex home configuration and evidence without copying shared host
|
|
12
|
+
history or ephemeral helper links. Reuse the provisioned shared-entry contract
|
|
13
|
+
and retain strict rejection of unrelated external recovery artifact links.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.39
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.40",
|
|
4
|
+
"cli_commit": "652524c6f370aa17c75474de5ebb7b3b9bf0b3a2",
|
|
5
|
+
"built_at": "2026-09-09T17:31:05.981Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.0",
|
|
8
8
|
"commit": "ef349bf47cba1c987468e51d73a0dbadbd48dc1f",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -59,6 +59,7 @@ import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntim
|
|
|
59
59
|
import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
|
|
60
60
|
import { launchRuntimeScopeControl, launchRuntimeScopeResume, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
|
|
61
61
|
import { launchRunController, resumeRunController, serveRunController, runControllerStatus, answerRunController, supplyRunControllerContext } from "../services/run-controller.js";
|
|
62
|
+
import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
|
|
62
63
|
import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
|
|
63
64
|
import { requestRunControl, serveRunControl } from "../services/run-control-worker.js";
|
|
64
65
|
import { startRuntimeService } from "../services/runtime-service.js";
|
|
@@ -1810,7 +1811,8 @@ async function dispatchRuntime(context, command, parsed, progress) {
|
|
|
1810
1811
|
}
|
|
1811
1812
|
const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
|
|
1812
1813
|
const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && 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.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
|
|
1813
|
-
|
|
1814
|
+
const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
|
|
1815
|
+
if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
|
|
1814
1816
|
throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
|
|
1815
1817
|
}
|
|
1816
1818
|
const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
|
|
@@ -14,6 +14,7 @@ import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
|
14
14
|
import { assertRunWorkResultArtifacts, runWorkResultArtifactIssues } from "./work-registry.js";
|
|
15
15
|
import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
|
|
16
16
|
import { resourceHome } from "./managed-processes.js";
|
|
17
|
+
import { codexHomeSharedEntries } from "./hooks.js";
|
|
17
18
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
18
19
|
export function createEvalBootstrapSnapshot(context, input) {
|
|
19
20
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -134,16 +135,17 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
134
135
|
fs.mkdirSync(output, { recursive: true, mode: 0o700 });
|
|
135
136
|
const resourceDatabase = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
136
137
|
const databaseFiles = [context.db.path, resourceDatabase].flatMap(file => [file, `${file}-wal`, `${file}-shm`]);
|
|
138
|
+
const codexExclusions = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
137
139
|
if (recovery) {
|
|
138
140
|
if (typeof status.run.run_root !== "string" || !isInside(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)) || samePath(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)))
|
|
139
141
|
throw new AppError("snapshot_run_scope_invalid", "Recovery requires a RUN artifact root within its runtime", 1);
|
|
140
142
|
for (const source of new Set([status.run.run_root, runtimeRunDir(context.ddFlowHome, project.id, input.runId)])) {
|
|
141
143
|
if (fs.existsSync(source))
|
|
142
|
-
copyRuntimeTree(source, path.join(output, "runtime", path.relative(context.ddFlowHome, source)));
|
|
144
|
+
copyRuntimeTree(source, path.join(output, "runtime", path.relative(context.ddFlowHome, source)), true, codexExclusions);
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
else
|
|
146
|
-
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, databaseFiles);
|
|
148
|
+
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions]);
|
|
147
149
|
// VACUUM INTO is a consistent SQLite read snapshot, including committed WAL
|
|
148
150
|
// pages. Copying the live database and its WAL separately is not atomic.
|
|
149
151
|
const databaseCopy = path.join(output, "runtime", path.relative(context.ddFlowHome, context.db.path));
|
|
@@ -734,6 +736,35 @@ function snapshotSourceVersion(context, input) {
|
|
|
734
736
|
...roots.map(root => [treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), gitBytes(root, ["show-ref", "--head"]).toString("base64"), indexEntries(root).toString("base64")])
|
|
735
737
|
])).digest("hex");
|
|
736
738
|
}
|
|
739
|
+
/** Managed Codex homes share host configuration, not owned snapshot payload. */
|
|
740
|
+
function codexHomeSnapshotExclusions(context, projectId, runId) {
|
|
741
|
+
const homes = new Set(context.db.all("SELECT target_home FROM codex_home_profiles WHERE project_id = ?", [projectId]).map(row => row.target_home));
|
|
742
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")) {
|
|
743
|
+
for (const { home } of context.db.all("SELECT json_extract(value, '$.codex_home') AS home FROM run_controllers, json_each(state_json, '$.sessions') WHERE project_id = ? AND run_id = ?", [projectId, runId]))
|
|
744
|
+
if (home)
|
|
745
|
+
homes.add(home);
|
|
746
|
+
}
|
|
747
|
+
const excluded = [];
|
|
748
|
+
for (const home of homes) {
|
|
749
|
+
if (!path.isAbsolute(home) || !fs.existsSync(home) || fs.lstatSync(home).isSymbolicLink())
|
|
750
|
+
continue;
|
|
751
|
+
const marker = path.join(home, ".dd-flow-home.json");
|
|
752
|
+
if (!fs.existsSync(marker) || fs.lstatSync(marker).isSymbolicLink())
|
|
753
|
+
continue;
|
|
754
|
+
const identity = JSON.parse(fs.readFileSync(marker, "utf8"));
|
|
755
|
+
if (identity.managed_by !== "dd-flow-cli" || identity.project_id !== projectId || identity.target_home !== home || !identity.source_home || !path.isAbsolute(identity.source_home))
|
|
756
|
+
continue;
|
|
757
|
+
for (const entry of codexHomeSharedEntries) {
|
|
758
|
+
const file = path.join(home, entry);
|
|
759
|
+
// Skip only the exact links installed by initCodexHome, never arbitrary
|
|
760
|
+
// external links or owned local files with the same convenient name.
|
|
761
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink() && fs.readlinkSync(file) === path.join(identity.source_home, entry))
|
|
762
|
+
excluded.push(file);
|
|
763
|
+
}
|
|
764
|
+
excluded.push(path.join(home, "tmp"), path.join(home, ".tmp"));
|
|
765
|
+
}
|
|
766
|
+
return excluded;
|
|
767
|
+
}
|
|
737
768
|
/** Copy a dedicated home or scoped recovery tree, preserving SQLite WAL commits. */
|
|
738
769
|
function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
739
770
|
if (fs.lstatSync(source).isSymbolicLink())
|
|
@@ -794,6 +825,7 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
|
794
825
|
}
|
|
795
826
|
function captureRecoveryAuxiliary(context, captured, input) {
|
|
796
827
|
const runtime = path.join(input.output, "runtime");
|
|
828
|
+
const codexExclusions = codexHomeSnapshotExclusions(context, input.projectId, input.runId);
|
|
797
829
|
const run = captured.db.get("SELECT run_root, project_root, workspace_root FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
798
830
|
const roots = [run.run_root, runtimeRunDir(context.ddFlowHome, input.projectId, input.runId)];
|
|
799
831
|
for (const protocol of captured.db.all("SELECT id, state_path, plan_path FROM protocols")) {
|
|
@@ -801,7 +833,7 @@ function captureRecoveryAuxiliary(context, captured, input) {
|
|
|
801
833
|
if (!isInside(physicalPath(context.ddFlowHome), physicalPath(directory)))
|
|
802
834
|
throw new AppError("snapshot_protocol_scope_invalid", "Protocol runtime leaves DD_FLOW_HOME", 1);
|
|
803
835
|
if (fs.existsSync(directory)) {
|
|
804
|
-
copyRuntimeTree(directory, path.join(runtime, path.relative(context.ddFlowHome, directory)));
|
|
836
|
+
copyRuntimeTree(directory, path.join(runtime, path.relative(context.ddFlowHome, directory)), true, codexExclusions);
|
|
805
837
|
roots.push(directory);
|
|
806
838
|
}
|
|
807
839
|
for (const file of [protocol.state_path, protocol.plan_path]) {
|
|
@@ -848,7 +880,7 @@ function captureRecoveryAuxiliary(context, captured, input) {
|
|
|
848
880
|
if (!owner || owner.project_id !== input.projectId || owner.run_id !== input.runId || owner.owner_id !== process.owner_id || owner.dd_flow_home !== context.ddFlowHome || owner.resource_home !== home || owner.state_dir !== directory)
|
|
849
881
|
throw new AppError("snapshot_native_scope_invalid", "External native artifact root has no matching RUN owner", 1, { state_dir: directory });
|
|
850
882
|
const relative = path.join("native-evidence", crypto.createHash("sha256").update(directory).digest("hex"));
|
|
851
|
-
copyRuntimeTree(directory, path.join(runtime, relative));
|
|
883
|
+
copyRuntimeTree(directory, path.join(runtime, relative), true, codexExclusions);
|
|
852
884
|
nativeCaptures.set(directory, relative);
|
|
853
885
|
}
|
|
854
886
|
const relative = path.join("resource-evidence", `${crypto.createHash("sha256").update(home).digest("hex")}.json`);
|
package/dist/services/hooks.js
CHANGED
|
@@ -14,7 +14,7 @@ import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
|
14
14
|
import { activeFlowSessionsForProject, bindObservedFlowSession, flowSessionPayloadFromRegisterCommand, recordFlowSessionObservation } from "./sessions.js";
|
|
15
15
|
const defaultProfileName = "default";
|
|
16
16
|
const maxSanitizedSummaryLength = 1600;
|
|
17
|
-
const
|
|
17
|
+
export const codexHomeSharedEntries = [
|
|
18
18
|
"auth.json",
|
|
19
19
|
"auth.bk",
|
|
20
20
|
"sessions",
|
|
@@ -51,7 +51,7 @@ export function planCodexHome(context, input) {
|
|
|
51
51
|
config_path: paths.configPath,
|
|
52
52
|
hooks_path: paths.hooksPath,
|
|
53
53
|
owned_files: ["config.toml", "hooks.json", ".dd-flow-home.json", "log/", ".tmp/", "tmp/"],
|
|
54
|
-
shared_entries:
|
|
54
|
+
shared_entries: codexHomeSharedEntries,
|
|
55
55
|
exists: fs.existsSync(paths.targetHome),
|
|
56
56
|
codex_env: { CODEX_HOME: paths.targetHome }
|
|
57
57
|
};
|
|
@@ -1118,7 +1118,7 @@ function objectTable(value) {
|
|
|
1118
1118
|
function createSharedLinks(sourceHome, targetHome) {
|
|
1119
1119
|
const linked = [];
|
|
1120
1120
|
const skipped = [];
|
|
1121
|
-
for (const entry of
|
|
1121
|
+
for (const entry of codexHomeSharedEntries) {
|
|
1122
1122
|
const source = path.join(sourceHome, entry);
|
|
1123
1123
|
const target = path.join(targetHome, entry);
|
|
1124
1124
|
if (!fs.existsSync(source)) {
|
|
@@ -1141,7 +1141,7 @@ function createSharedLinks(sourceHome, targetHome) {
|
|
|
1141
1141
|
return { linked, skipped };
|
|
1142
1142
|
}
|
|
1143
1143
|
function removeSharedSymlinks(targetHome) {
|
|
1144
|
-
for (const entry of
|
|
1144
|
+
for (const entry of codexHomeSharedEntries) {
|
|
1145
1145
|
const target = path.join(targetHome, entry);
|
|
1146
1146
|
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
|
|
1147
1147
|
fs.rmSync(target);
|
|
@@ -1154,7 +1154,7 @@ function removeDirIfEmpty(dir) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
}
|
|
1156
1156
|
function sharedEntryStatus(sourceHome, targetHome) {
|
|
1157
|
-
return
|
|
1157
|
+
return codexHomeSharedEntries.map((entry) => {
|
|
1158
1158
|
const source = path.join(sourceHome, entry);
|
|
1159
1159
|
const target = path.join(targetHome, entry);
|
|
1160
1160
|
return {
|
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { AppError } from "../shared/errors.js";
|
|
3
4
|
import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
|
|
4
5
|
import { runtimeHarness } from "./execution-policy.js";
|
|
5
|
-
import { appendEvent, digest, errorRecord } from "./run-controller-state.js";
|
|
6
|
+
import { appendEvent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
7
|
+
/** A paused RUN admits only its controller's exact accepted answer Turn. */
|
|
8
|
+
export function controllerHitlAdmission(context, record, input) {
|
|
9
|
+
if (input.operation !== "session.prompt" || !input.operationId || !input.promptSha256)
|
|
10
|
+
return false;
|
|
11
|
+
if (!context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controller_operations'"))
|
|
12
|
+
return false;
|
|
13
|
+
const row = context.db.get("SELECT * FROM run_controllers WHERE controller_id = ? AND project_id = ? AND run_id = ? AND status = 'waiting_for_user'", [record.owner_id, record.project_id, record.run_id]);
|
|
14
|
+
if (!row || row.generation !== JSON.parse(record.metadata_json).run_generation)
|
|
15
|
+
return false;
|
|
16
|
+
const suffix = `:prompt:${row.generation}`;
|
|
17
|
+
if (!input.operationId.endsWith(suffix))
|
|
18
|
+
return false;
|
|
19
|
+
const state = JSON.parse(row.state_json);
|
|
20
|
+
const session = state.current_session === null ? undefined : state.sessions[state.current_session];
|
|
21
|
+
if (!session || session.stopped || session.id !== input.nativeSessionId || session.state_dir !== input.stateDir || session.owner.owner_id !== record.owner_id)
|
|
22
|
+
return false;
|
|
23
|
+
const operation = (id) => context.db.get("SELECT input_json, input_hash FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'dispatching'", [id, row.controller_id, row.generation]);
|
|
24
|
+
const answer = context.db.get("SELECT input_json, input_hash FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation <= ? AND status = 'accepted'", [input.operationId.slice(0, -suffix.length), row.controller_id, row.generation]);
|
|
25
|
+
const prompt = operation(input.operationId);
|
|
26
|
+
if (!answer || !prompt)
|
|
27
|
+
return false;
|
|
28
|
+
const accepted = JSON.parse(answer.input_json);
|
|
29
|
+
const dispatch = JSON.parse(prompt.input_json);
|
|
30
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
|
|
31
|
+
const stage = run && JSON.parse(run.index_json).stage_runs.find(item => item.stage === accepted.stage);
|
|
32
|
+
if (accepted.action !== "user_answer" || state.stage !== accepted.stage || stage?.status !== "paused" || !stage.pause || stage.pause.answer_path || stage.pause.id !== accepted.pause_id || stage.pause.work_id !== accepted.work_id)
|
|
33
|
+
return false;
|
|
34
|
+
if (answer.input_hash !== digest({ pause_id: accepted.pause_id, answer_sha256: accepted.sha256 }) || prompt.input_hash !== digest(dispatch))
|
|
35
|
+
return false;
|
|
36
|
+
const value = (option) => dispatch.args.includes(option) ? dispatch.args[dispatch.args.indexOf(option) + 1] : undefined;
|
|
37
|
+
if (dispatch.owner !== record.owner_id || dispatch.state_dir !== input.stateDir || dispatch.args[0] !== "session" || dispatch.args[1] !== "prompt" || value("--session-id") !== input.nativeSessionId || dispatch.prompt_sha256 !== input.promptSha256 || !value("--prompt-file"))
|
|
38
|
+
return false;
|
|
39
|
+
return digestBytes(fs.readFileSync(accepted.file)) === accepted.sha256 && digestBytes(fs.readFileSync(value("--prompt-file"))) === input.promptSha256;
|
|
40
|
+
}
|
|
6
41
|
export function sessionArguments(context, manifest, session) {
|
|
7
42
|
const profile = session.profile;
|
|
8
43
|
return ["--dd-flow-bin", manifest.flow_executable, "--dd-flow-home", context.ddFlowHome, "--project-root", manifest.request.project_root, "--state-dir", session.state_dir, "--journal", session.journal, "--cwd", session.cwd, "--provider", profile.provider, "--model", profile.model, "--reasoning", profile.reasoning, "--mode", profile.mode, "--permission", profile.permission, ...manifest.adapters[runtimeHarness(profile)].runtime_args, "--json"];
|
|
@@ -19,7 +54,7 @@ export async function controllerAdapter(context, row, manifest, session, args, a
|
|
|
19
54
|
assertOwner?.();
|
|
20
55
|
const operationId = retainedOperationId ?? `${row.controller_id}:${crypto.randomUUID()}`;
|
|
21
56
|
const executable = manifest.adapters[runtimeHarness(session.profile)].executable;
|
|
22
|
-
const input = { executable, args, owner: session.owner.owner_id, state_dir: session.state_dir };
|
|
57
|
+
const input = { executable, args, owner: session.owner.owner_id, state_dir: session.state_dir, ...(args[0] === "session" && args[1] === "prompt" ? { prompt_sha256: digestBytes(fs.readFileSync(args[args.indexOf("--prompt-file") + 1])) } : {}) };
|
|
23
58
|
const now = context.now();
|
|
24
59
|
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]);
|
|
25
60
|
appendEvent(context, row.controller_id, "operation_requested", { operation_id: operationId, operation: args.slice(0, 2), session_id: session.id || null });
|
|
@@ -342,12 +342,12 @@ async function executeController(context, row, manifest, state, assertOwner, ass
|
|
|
342
342
|
if (failure)
|
|
343
343
|
throw failure.reason;
|
|
344
344
|
};
|
|
345
|
-
const prompt = async (session, text) => {
|
|
345
|
+
const prompt = async (session, text, operationId) => {
|
|
346
346
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
347
347
|
fs.writeFileSync(file, `${text}\n`, { flag: "wx", mode: 0o600 });
|
|
348
348
|
state.turns += 1;
|
|
349
349
|
saveState(context, row, state);
|
|
350
|
-
const receipt = await
|
|
350
|
+
const receipt = await controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, operationId);
|
|
351
351
|
state.last_receipt = receipt;
|
|
352
352
|
saveState(context, row, state);
|
|
353
353
|
return receipt;
|
|
@@ -391,9 +391,10 @@ async function executeController(context, row, manifest, state, assertOwner, ass
|
|
|
391
391
|
if (digestBytes(fs.readFileSync(input.file)) !== input.sha256)
|
|
392
392
|
throw new AppError("context_checksum_mismatch", "Accepted HITL answer bytes changed", 1);
|
|
393
393
|
assertOwner();
|
|
394
|
-
|
|
394
|
+
// The accepted answer is logical intent; only its generation-bound
|
|
395
|
+
// adapter operation represents a physical dispatch that stop must drain.
|
|
395
396
|
const command = `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${JSON.stringify(run.project_root)} --answer-stdin --json < ${JSON.stringify(input.file)}`;
|
|
396
|
-
const receipt = await prompt(state.sessions[state.current_session], `The user supplied the raw answer for pause ${pause.id}. Your first tool call must be this exact standalone command:\n\n${command}\n\nDo not alter the answer. Follow the returned packet, then stop at this Stage boundary
|
|
397
|
+
const receipt = await prompt(state.sessions[state.current_session], `The user supplied the raw answer for pause ${pause.id}. Your first tool call must be this exact standalone command:\n\n${command}\n\nDo not alter the answer. Follow the returned packet, then stop at this Stage boundary.`, `${answer.operation_id}:prompt:${row.generation}`);
|
|
397
398
|
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ delivered: true, session_id: adapterSessionId(receipt) }), context.now(), answer.operation_id]);
|
|
398
399
|
state.last_continuation = null;
|
|
399
400
|
saveState(context, row, state);
|