fix(pi-workflow): reset failed verification on retry

This commit is contained in:
jb
2026-08-25 19:50:22 -04:00
parent 805b8e00d4
commit a54a92ace4
4 changed files with 34 additions and 1 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ An analysis run uses `action: "start"` with `workflow: { mode: "analysis-script"
## Recovery ## Recovery
Interrupted work opens a Recovery Gate with retry, inspect, and abort choices. Completed Assignments and Decisions remain persisted. Potentially mutating interrupted Assignments are preserved and are not replayed automatically; an explicit retry creates newly fenced work where policy permits it. Startup recovery leaves an idle run already waiting at Recovery under its current parent instead of letting another Pi session claim it. If that parent's process has exited, an explicit retry from a previously attached parent session may reclaim the stale lease before continuing. Interrupted work opens a Recovery Gate with retry, inspect, and abort choices. Completed Assignments and Decisions remain persisted. Potentially mutating interrupted Assignments are preserved and are not replayed automatically; an explicit retry creates newly fenced work where policy permits it. Startup recovery leaves an idle run already waiting at Recovery under its current parent instead of letting another Pi session claim it. If that parent's process has exited, an explicit retry from a previously attached parent session may reclaim the stale lease before continuing. Retry clears prior failed-command output and reruns verification from a pending state.
## Artifacts ## Artifacts
+1
View File
@@ -519,6 +519,7 @@ On process or session interruption:
- Active Assignments become `interrupted` idempotently. - Active Assignments become `interrupted` idempotently.
- Potentially mutating Assignments are never replayed automatically. - Potentially mutating Assignments are never replayed automatically.
- The run opens a Recovery Gate offering retry, inspect, or abort. - The run opens a Recovery Gate offering retry, inspect, or abort.
- Explicit retry resets failed verification commands to pending and clears their prior output before host execution.
- Completed Assignments and accepted Decisions remain intact. - Completed Assignments and accepted Decisions remain intact.
## Child Execution ## Child Execution
+13
View File
@@ -1512,9 +1512,20 @@ test("Recovery retry reruns failed verification after the remediation limit", as
await withTempDir(async (directory) => { await withTempDir(async (directory) => {
const setup = await fixture(directory); const setup = await fixture(directory);
let commandRuns = 0; let commandRuns = 0;
let runId: string | undefined;
const hostCommands: HostCommandExecutor = { const hostCommands: HostCommandExecutor = {
async run() { async run() {
commandRuns++; commandRuns++;
if (commandRuns === 2) {
assert.ok(runId);
const retrying = await setup.store.loadRun(runId);
const [command] = retrying.verificationEvidence?.commands ?? [];
assert.equal(command?.status, "running");
assert.equal(command?.stdout, undefined);
assert.equal(command?.stderr, undefined);
assert.equal(command?.exitCode, undefined);
assert.equal(command?.error, undefined);
}
return { return {
result: { result: {
code: commandRuns === 1 ? 1 : 0, code: commandRuns === 1 ? 1 : 0,
@@ -1552,6 +1563,7 @@ test("Recovery retry reruns failed verification after the remediation limit", as
hostCommands, hostCommands,
effectiveConfig, effectiveConfig,
}).execute(startCommand()); }).execute(startCommand());
runId = planned.runId;
const approved = await setup.engine("session-1", { const approved = await setup.engine("session-1", {
hostCommands, hostCommands,
effectiveConfig, effectiveConfig,
@@ -1598,6 +1610,7 @@ test("Recovery retry reruns failed verification after the remediation limit", as
const liveStolen = structuredClone(untouched); const liveStolen = structuredClone(untouched);
liveStolen.revision++; liveStolen.revision++;
liveStolen.parentSessionIds.push("transient-session"); liveStolen.parentSessionIds.push("transient-session");
liveStolen.verificationEvidence!.commands[0]!.error = "stale host exception";
liveStolen.lease = { liveStolen.lease = {
processId: 200, processId: 200,
activeParentSessionId: "transient-session", activeParentSessionId: "transient-session",
+19
View File
@@ -4279,6 +4279,10 @@ export class WorkflowEngine {
run.verificationEvidence?.commands.filter( run.verificationEvidence?.commands.filter(
(command) => command.status === "running" || command.status === "interrupted", (command) => command.status === "running" || command.status === "interrupted",
) ?? []; ) ?? [];
const failedCommands =
run.verificationEvidence?.commands.filter(
(command) => command.status === "failed",
) ?? [];
run = await this.transition(run, (next) => { run = await this.transition(run, (next) => {
for (const assignment of next.assignments) { for (const assignment of next.assignments) {
if ( if (
@@ -4309,6 +4313,21 @@ export class WorkflowEngine {
next.script.error = undefined; next.script.error = undefined;
} }
if (next.verificationEvidence) { if (next.verificationEvidence) {
for (const command of failedCommands) {
const persisted = next.verificationEvidence.commands.find(
(candidate) => candidate.id === command.id,
);
if (!persisted) continue;
persisted.status = "pending";
persisted.exitCode = undefined;
persisted.stdout = undefined;
persisted.stderr = undefined;
persisted.error = undefined;
}
if (failedCommands.length > 0) {
next.verificationEvidence.focusedBehavioralTests = false;
next.verificationEvidence.verifierSucceeded = false;
}
for (const command of interruptedCommands) { for (const command of interruptedCommands) {
const persisted = next.verificationEvidence.commands.find( const persisted = next.verificationEvidence.commands.find(
(candidate) => candidate.id === command.id, (candidate) => candidate.id === command.id,