@deksden-com/dd-flow-cli 0.9.0-beta.121 → 0.9.0-beta.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/hook-ingress.js +4 -3
- package/dist/cli/native-hook-ingress.js +34 -2
- package/dist/cli/run-cli.js +3 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +68 -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-daemon.mjs +60 -8
- package/dist/harness-runtime/lib/dd-zcode.mjs +40 -16
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- 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/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/services/codex-capacity.js +131 -12
- package/dist/services/codex-hook-delivery.js +6 -2
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +27 -9
- package/dist/services/hooks.js +51 -59
- package/dist/services/lifecycle-command.js +1 -383
- package/dist/services/lifecycle-contract.js +1 -30
- package/dist/services/lifecycle-invocations.js +34 -13
- 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/runs.js +60 -47
- package/dist/services/stage-report-renderer.js +44 -2
- package/dist/services/verification-matrix.js +345 -0
- package/dist/services/vnext-code-review.js +30 -11
- package/dist/services/vnext-code.js +31 -10
- package/dist/services/vnext-merge.js +30 -7
- package/dist/services/vnext-plan-review.js +28 -7
- package/dist/services/vnext-plan.js +16 -3
- package/dist/services/work-registry.js +15 -1
- package/dist/storage/database.js +43 -9
- package/package.json +4 -1
|
@@ -777,7 +777,8 @@ export function issueLifecycleInvocation(context, command, scope) {
|
|
|
777
777
|
}
|
|
778
778
|
function commandWithInvocationId(command, id) {
|
|
779
779
|
const at = shellSuffixOffset(command);
|
|
780
|
-
|
|
780
|
+
const suffix = command.slice(at);
|
|
781
|
+
return `${command.slice(0, at)} --invocation-id ${id}${suffix ? /^\s/.test(suffix) ? suffix : ` ${suffix}` : ""}`;
|
|
781
782
|
}
|
|
782
783
|
/** The attempt owns authority; the caller owns presentation-only shell input.
|
|
783
784
|
* Preserve current stdin/heredoc/response-file text while validating that it
|
|
@@ -909,11 +910,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
909
910
|
storage(context);
|
|
910
911
|
const supplied = input.id ? context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [input.id]) : undefined;
|
|
911
912
|
const analysis = parseLifecycleCommand(input.command);
|
|
912
|
-
if (analysis.kind
|
|
913
|
-
if (analysis.kind !== "none" && ["grok-acp", "antigravity-cli"].includes(input.harness ?? "") && input.projectId
|
|
914
|
-
&& managedNativeOwner(context, input.identity, input.harness, input.projectId)) {
|
|
915
|
-
throw new AppError("invocation_command_invalid", "Managed native lifecycle call must be one direct standalone command", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
916
|
-
}
|
|
913
|
+
if (analysis.kind === "none" || analysis.supported === false) {
|
|
917
914
|
const eventKey = input.recordReceipt();
|
|
918
915
|
if (analysis.kind !== "none") {
|
|
919
916
|
const retained = context.db.get("SELECT outcome_json FROM hook_events WHERE event_key = ?", [eventKey]);
|
|
@@ -928,22 +925,32 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
928
925
|
...outcome,
|
|
929
926
|
lifecycle_observation: {
|
|
930
927
|
code: "lifecycle_shell_syntax_invalid",
|
|
931
|
-
reason: analysis.kind === "compound" ? analysis.reason : "
|
|
928
|
+
reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command",
|
|
932
929
|
command_fingerprint: fingerprint(analysis.invocation),
|
|
933
930
|
effect: "no_effect",
|
|
934
931
|
recoverable: true
|
|
935
932
|
}
|
|
936
933
|
}), eventKey]);
|
|
937
934
|
}
|
|
935
|
+
if (analysis.kind !== "none")
|
|
936
|
+
throw new AppError("lifecycle_shell_syntax_invalid", "This shell form cannot bind exactly one lifecycle execution; use the exact issued lifecycle command without the unsupported shell context", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.kind === "compound" ? analysis.reason : "unsupported_lifecycle_command", event_key: eventKey });
|
|
938
937
|
return { eventKey, duplicate: false, invocationId: null };
|
|
939
938
|
}
|
|
940
|
-
|
|
939
|
+
// Admit the one parsed leaf, never execute or fingerprint the prefix/suffix.
|
|
940
|
+
const leafCommand = analysis.invocation.command;
|
|
941
|
+
if (input.executionCwd !== undefined && !path.isAbsolute(input.executionCwd))
|
|
942
|
+
throw new AppError("invocation_directory_mismatch", "Bound lifecycle execution cwd must be absolute", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
943
|
+
const deliveredIdentity = JSON.stringify([input.identity.daemonId, input.identity.rootSessionId, input.identity.sessionId, input.identity.parentSessionId, input.identity.toolCallId,
|
|
944
|
+
...(input.executionCwd === undefined ? [] : [{ executionCwd: canonicalPath(input.executionCwd) }])]);
|
|
945
|
+
const candidates = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing','settled') ORDER BY rowid DESC")
|
|
941
946
|
.filter(row => {
|
|
947
|
+
if (["executing", "settled"].includes(row.status) && row.identity_json !== deliveredIdentity)
|
|
948
|
+
return false;
|
|
942
949
|
const scope = JSON.parse(row.scope_json);
|
|
943
950
|
if (scope.daemonId !== input.identity.daemonId || scope.rootSessionId !== input.identity.rootSessionId)
|
|
944
951
|
return false;
|
|
945
952
|
try {
|
|
946
|
-
return fingerprint(invocation(canonicalManagedCommand(context,
|
|
953
|
+
return fingerprint(invocation(canonicalManagedCommand(context, leafCommand, scope))) === row.fingerprint;
|
|
947
954
|
}
|
|
948
955
|
catch {
|
|
949
956
|
return false;
|
|
@@ -979,7 +986,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
979
986
|
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
980
987
|
const identity = input.identity;
|
|
981
988
|
checkNativeIdentity(scope, identity, initial.id);
|
|
982
|
-
const identityJson =
|
|
989
|
+
const identityJson = deliveredIdentity;
|
|
983
990
|
return context.db.writeTransaction(() => {
|
|
984
991
|
const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [initial.id]);
|
|
985
992
|
if (row.status === "settled")
|
|
@@ -992,6 +999,9 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
992
999
|
if (row.identity_json === identityJson) {
|
|
993
1000
|
return { eventKey: row.event_key, duplicate: true, ...resolved };
|
|
994
1001
|
}
|
|
1002
|
+
if (row.identity_json && JSON.stringify(JSON.parse(row.identity_json).slice(0, 5)) === JSON.stringify(JSON.parse(identityJson).slice(0, 5))) {
|
|
1003
|
+
throw new AppError("invocation_identity_mismatch", "The same native tool call changed its bound execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
1004
|
+
}
|
|
995
1005
|
const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
|
|
996
1006
|
// A native hook may execute from a provider worktree while the issued
|
|
997
1007
|
// lifecycle scope is anchored to the stable project root. Project
|
|
@@ -1019,7 +1029,7 @@ export function observedLifecycleNativeEvent(context, command, daemonId) {
|
|
|
1019
1029
|
return null;
|
|
1020
1030
|
const signature = (value) => {
|
|
1021
1031
|
const parsed = parseLifecycleCommand(value);
|
|
1022
|
-
const candidate = parsed.kind
|
|
1032
|
+
const candidate = parsed.kind !== "none" && parsed.supported ? parsed.invocation : continuationCommand(value);
|
|
1023
1033
|
if (!candidate)
|
|
1024
1034
|
return null;
|
|
1025
1035
|
return JSON.stringify([candidate.operation, candidate.args.positional,
|
|
@@ -1080,8 +1090,9 @@ export function observedLifecycleInvocation(context, command, daemonId) {
|
|
|
1080
1090
|
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'"))
|
|
1081
1091
|
return null;
|
|
1082
1092
|
const analysis = parseLifecycleCommand(command);
|
|
1083
|
-
if (analysis.kind
|
|
1093
|
+
if (analysis.kind === "none" || analysis.supported === false)
|
|
1084
1094
|
return null;
|
|
1095
|
+
const leafCommand = analysis.invocation.command;
|
|
1085
1096
|
// `issued` is a rendezvous, not authority: the CLI still blocks in
|
|
1086
1097
|
// awaitLifecycleInvocation until the native observer commits the receipt.
|
|
1087
1098
|
const matches = context.db.all("SELECT * FROM lifecycle_invocations WHERE status IN ('issued','observed','executing') ORDER BY rowid DESC")
|
|
@@ -1090,7 +1101,7 @@ export function observedLifecycleInvocation(context, command, daemonId) {
|
|
|
1090
1101
|
const scope = JSON.parse(row.scope_json);
|
|
1091
1102
|
if (daemonId && scope.daemonId !== daemonId)
|
|
1092
1103
|
return false;
|
|
1093
|
-
return fingerprint(invocation(canonicalManagedCommand(context,
|
|
1104
|
+
return fingerprint(invocation(canonicalManagedCommand(context, leafCommand, scope))) === row.fingerprint;
|
|
1094
1105
|
}
|
|
1095
1106
|
catch {
|
|
1096
1107
|
return false;
|
|
@@ -1348,6 +1359,16 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
1348
1359
|
}
|
|
1349
1360
|
input.assertCurrent(JSON.parse(row.scope_json));
|
|
1350
1361
|
if (row.status === "observed") {
|
|
1362
|
+
if (row.identity_json) {
|
|
1363
|
+
// Receipt display text is bounded/redacted and may not be valid JSON.
|
|
1364
|
+
// Only the native admission's typed identity binds an execution route.
|
|
1365
|
+
const identity = JSON.parse(row.identity_json);
|
|
1366
|
+
const route = identity[5]?.executionCwd;
|
|
1367
|
+
if (identity.length > 5 && (typeof route !== "string" || !path.isAbsolute(route)))
|
|
1368
|
+
throw new AppError("invocation_directory_mismatch", "Bound lifecycle execution cwd is invalid", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
1369
|
+
if (typeof route === "string" && canonicalPath(process.cwd()) !== canonicalPath(route))
|
|
1370
|
+
throw new AppError("invocation_directory_mismatch", "Reached CLI cwd differs from the bound shell execution route", 1, { phase: "prepare", effect: "no_effect", recoverable: false, expected: route, actual: process.cwd(), actual_provenance: "cli_process_cwd" });
|
|
1371
|
+
}
|
|
1351
1372
|
const claimed = context.db.run("UPDATE lifecycle_invocations SET status = 'executing', updated_at = ? WHERE id = ? AND status = 'observed'", [context.now(), input.id]);
|
|
1352
1373
|
if (claimed.changes === 1)
|
|
1353
1374
|
return { eventKey: row.event_key, replay: false };
|
|
@@ -109,7 +109,7 @@ async function dispatch(context, input) {
|
|
|
109
109
|
const retain = () => { context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify(retained), context.now(), input.request.merge_request_id, input.serverId, token]); };
|
|
110
110
|
retain();
|
|
111
111
|
const leaseHeartbeat = setInterval(() => { if (guarded(context, input.request) || (recoveryGuard(context, input.request.project_id, input.request.run_id)?.generation ?? 0) !== generation)
|
|
112
|
-
return; const now = context.now(); context.db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE merge_request_id = ? AND status
|
|
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) });
|
|
@@ -30,7 +30,7 @@ import { requireRun, requireController, controllerReceipt, appendEvent, saveStat
|
|
|
30
30
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
31
31
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady } from "./run-controller-process.js";
|
|
32
32
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
33
|
-
import { continueCodexCapacity } from "./codex-capacity.js";
|
|
33
|
+
import { capacityDelay, continueCodexCapacity } from "./codex-capacity.js";
|
|
34
34
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
35
35
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
36
36
|
import { requestRunControl } from "./run-control-worker.js";
|
|
@@ -534,8 +534,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
534
534
|
await reconcilePromptPredecessor(session);
|
|
535
535
|
const firstOperationId = operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
536
536
|
const send = (value, id) => {
|
|
537
|
+
const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
|
|
538
|
+
if (retained) {
|
|
539
|
+
const dispatch = JSON.parse(retained.input_json);
|
|
540
|
+
if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
|
|
541
|
+
throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
|
|
542
|
+
return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal);
|
|
543
|
+
}
|
|
537
544
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
538
|
-
fs.writeFileSync(file,
|
|
545
|
+
fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
|
|
539
546
|
state.turns += 1;
|
|
540
547
|
saveState(context, row, state);
|
|
541
548
|
return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal);
|
|
@@ -553,24 +560,45 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
553
560
|
throw error;
|
|
554
561
|
const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
|
|
555
562
|
args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
|
|
556
|
-
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId,
|
|
563
|
+
const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true,
|
|
564
|
+
retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND operation_id IN (?, ?)", [row.controller_id, row.generation, firstOperationId, `${firstOperationId}:capacity:1:session.prompt`])
|
|
565
|
+
.flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
|
|
557
566
|
inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
|
|
558
|
-
inspectSession:
|
|
567
|
+
inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
|
|
559
568
|
assertCurrent: () => {
|
|
560
569
|
assertOwner();
|
|
570
|
+
},
|
|
571
|
+
decision: async () => {
|
|
572
|
+
if (kind === "child") {
|
|
573
|
+
const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
|
|
574
|
+
if (!target)
|
|
575
|
+
return "reconciliation_blocked";
|
|
576
|
+
const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
|
|
577
|
+
const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
|
|
578
|
+
if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
|
|
579
|
+
return "completed";
|
|
580
|
+
}
|
|
561
581
|
const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
562
582
|
const next = view.continuation;
|
|
583
|
+
if (next.kind === "blocked")
|
|
584
|
+
return "reconciliation_blocked";
|
|
585
|
+
if (next.kind === "paused" && next.stage !== state.stage)
|
|
586
|
+
return "paused";
|
|
587
|
+
if (next.kind === "terminal" || next.stage !== state.stage)
|
|
588
|
+
return "completed";
|
|
563
589
|
if (hitl) {
|
|
564
590
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
565
591
|
const answer = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status = 'accepted'", [answerId, row.controller_id]);
|
|
566
592
|
const accepted = answer && JSON.parse(answer.input_json);
|
|
567
593
|
const pause = view.index.stage_runs.find(item => item.stage === accepted?.stage)?.pause;
|
|
568
|
-
if (next.kind !== "paused"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
throw new AppError("capacity_continuation_blocked", "RUN changed while native capacity was unavailable", 1, { continuation: next });
|
|
594
|
+
if (next.kind !== "paused")
|
|
595
|
+
return "continue";
|
|
596
|
+
if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
|
|
597
|
+
return "paused";
|
|
573
598
|
}
|
|
599
|
+
else if (next.kind === "paused")
|
|
600
|
+
return "paused";
|
|
601
|
+
return next.kind === "wait" ? "reconciliation_blocked" : "continue";
|
|
574
602
|
},
|
|
575
603
|
retainIntent: intent => context.db.writeTransaction(() => {
|
|
576
604
|
const changed = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
|
|
@@ -579,7 +607,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
579
607
|
appendEvent(context, row.controller_id, "capacity_continuation", intent);
|
|
580
608
|
}),
|
|
581
609
|
prompt: (id, value) => send(value, id),
|
|
582
|
-
...(resumeCommand ? { continuationText: `
|
|
610
|
+
...(resumeCommand ? { continuationText: `Continue the same accepted HITL task in this Session. First inspect current durable RUN and pause state. Only if the exact pause still awaits the accepted answer, run this previously issued command once:\n\n${resumeCommand}\n\nNever replay an already committed answer. Otherwise use only the current issued successor.` } : {}) });
|
|
583
611
|
if (!resumed)
|
|
584
612
|
throw error;
|
|
585
613
|
receipt = resumed;
|
|
@@ -773,18 +801,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
773
801
|
const prior = child && state.child_capacity?.[child.session_id];
|
|
774
802
|
const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
|
|
775
803
|
if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
|
|
776
|
-
&& child.native_turn_items?.observed === true && child.native_turn_items.possible_effects === false
|
|
804
|
+
&& child.native_turn_items?.observed === true && (child.native_turn_items.possible_effects === false || child.native_turn_items.pending === false)
|
|
777
805
|
&& inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
|
|
778
|
-
&& (!links.length || links[0].status === "running") && (prior?.attempts ?? 0) < 2) {
|
|
779
|
-
|
|
806
|
+
&& (!links.length || links[0].status === "running") && ((prior?.attempts ?? 0) < 2 || prior?.turn_id === turnId && prior.dispatched === false)) {
|
|
807
|
+
const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
|
|
808
|
+
if (prior?.turn_id === turnId && !prepared)
|
|
780
809
|
throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
|
|
781
|
-
const attempt = (prior?.attempts ?? 0) + 1;
|
|
810
|
+
const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
|
|
811
|
+
const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
|
|
812
|
+
if (!Number.isFinite(wait) || wait > 30_000)
|
|
813
|
+
throw new AppError("capacity_child_retry_delay_exceeded", "Native child retry delay exceeds the bounded continuation window", 1, { child_session_id: child.session_id, delay_ms: wait });
|
|
814
|
+
const operationId = prepared?.operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
|
|
782
815
|
state.child_capacity ??= {};
|
|
783
|
-
state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt };
|
|
816
|
+
state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
|
|
784
817
|
saveState(context, row, state);
|
|
785
|
-
const operationId = `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
|
|
786
818
|
appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
|
|
787
|
-
await delay(
|
|
819
|
+
await delay(wait, undefined, { signal: cancellation.signal });
|
|
788
820
|
assertOwner();
|
|
789
821
|
const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
790
822
|
if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
|
|
@@ -792,13 +824,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
792
824
|
const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
793
825
|
const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
|
|
794
826
|
const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
|
|
827
|
+
if (still?.status === "completed" && currentLinks.length <= 1 && currentLinks.every(link => link.status === "completed")) {
|
|
828
|
+
state.last_receipt = fresh;
|
|
829
|
+
saveState(context, row, state);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
795
832
|
if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
|
|
796
|
-
|| still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false || currentLinks.length > 1
|
|
833
|
+
|| still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false && still.native_turn_items.pending !== false || currentLinks.length > 1
|
|
797
834
|
|| currentLinks.some(link => link.status !== "running"))
|
|
798
835
|
throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
|
|
799
836
|
assertOwner();
|
|
800
|
-
const text = `The existing native child ${child.session_id} has a confirmed
|
|
837
|
+
const text = `The existing native child ${child.session_id} has a confirmed terminal overloaded Turn. Use the native followup_task tool exactly once for that same child ID with this short message: "Continue your assigned Work from its current durable state. Do not repeat an observed lifecycle command. Finish only the original Work." Do not spawn another child, claim another Work, or perform the child's Work in this parent Turn. End this Turn after the followup_task response.`;
|
|
801
838
|
await prompt(session, text, operationId, "child");
|
|
839
|
+
state.child_capacity[child.session_id].dispatched = true;
|
|
840
|
+
saveState(context, row, state);
|
|
802
841
|
const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
803
842
|
state.last_receipt = after;
|
|
804
843
|
saveState(context, row, state);
|
|
@@ -829,7 +868,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
829
868
|
const results = await Promise.allSettled(fanout.works.map(async (work) => {
|
|
830
869
|
try {
|
|
831
870
|
assertOwner();
|
|
832
|
-
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage });
|
|
871
|
+
const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
|
|
833
872
|
if (fatal)
|
|
834
873
|
throw fatal;
|
|
835
874
|
appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
|
|
@@ -90,7 +90,7 @@ export function runObservations(context, projectId, runId) {
|
|
|
90
90
|
const actual = fs.realpathSync(file);
|
|
91
91
|
if (!inside(home, actual))
|
|
92
92
|
throw new Error("journal escaped owned home");
|
|
93
|
-
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
93
|
+
fd = fs.openSync(actual, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
94
94
|
if (!fs.fstatSync(fd).isFile())
|
|
95
95
|
throw new Error("journal is not a regular file");
|
|
96
96
|
replayToolJournal(file, { harness: source.session.harness_id, rootSessionId: sessionId, text: fs.readFileSync(fd, "utf8"), ...(source.session.harness_id === "codex-desktop" ? { expectedSessionIds: expectedCodexIds } : {}) }, reducer);
|
|
@@ -114,7 +114,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
114
114
|
// Select transcript fallback only after replay reveals an actual gap.
|
|
115
115
|
for (const session of sessions) {
|
|
116
116
|
const nativeId = session.provider_session_id ?? session.session_id;
|
|
117
|
-
|
|
117
|
+
const sessionSummary = reducer.summary(nativeId);
|
|
118
|
+
if (!session.transcript_path || covered().has(JSON.stringify({ harness_id: session.harness, session_id: nativeId }))
|
|
119
|
+
&& sessionSummary.completeness === "complete" && sessionSummary.outcome_completeness === "complete")
|
|
118
120
|
continue;
|
|
119
121
|
const id = `transcript:${session.harness}:${nativeId}`;
|
|
120
122
|
add(id, nativeId, session.transcript_path, home, "current", session.harness);
|
|
@@ -136,6 +138,9 @@ export function runObservations(context, projectId, runId) {
|
|
|
136
138
|
if (!expectedIds.has(JSON.stringify(event.session))) {
|
|
137
139
|
reducer.turns.delete(key);
|
|
138
140
|
}
|
|
141
|
+
for (const [key, event] of reducer.sources)
|
|
142
|
+
if (!expectedIds.has(JSON.stringify(event.session)))
|
|
143
|
+
reducer.sources.delete(key);
|
|
139
144
|
const summary = reducer.summary();
|
|
140
145
|
const observed = new Set(summary.sessions.map(s => JSON.stringify(s.session)));
|
|
141
146
|
const missing = expected.filter(s => !observed.has(JSON.stringify(s)));
|