diff --git a/docs/pi-workflow/README.md b/docs/pi-workflow/README.md index 09de2a8..265ee80 100644 --- a/docs/pi-workflow/README.md +++ b/docs/pi-workflow/README.md @@ -70,7 +70,7 @@ An analysis run uses `action: "start"` with `workflow: { mode: "analysis-script" ## 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. Retry clears prior failed-command output and reruns verification from a pending state. +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. Once remediation completes the Implement stage, an older interrupted mutating Assignment no longer triggers duplicate remediation on later retries. ## Artifacts diff --git a/docs/pi-workflow/design.md b/docs/pi-workflow/design.md index 08dba33..e8a7e07 100644 --- a/docs/pi-workflow/design.md +++ b/docs/pi-workflow/design.md @@ -520,6 +520,7 @@ On process or session interruption: - Potentially mutating Assignments are never replayed automatically. - 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. +- An interrupted mutating Assignment routes to remediation only while the Implement stage is incomplete; after remediation completes that stage, later retries continue into verification. - Completed Assignments and accepted Decisions remain intact. ## Child Execution diff --git a/extensions/pi-workflow/engine.test.ts b/extensions/pi-workflow/engine.test.ts index 13ac6dd..eb18cbd 100644 --- a/extensions/pi-workflow/engine.test.ts +++ b/extensions/pi-workflow/engine.test.ts @@ -1611,6 +1611,11 @@ test("Recovery retry reruns failed verification after the remediation limit", as liveStolen.revision++; liveStolen.parentSessionIds.push("transient-session"); liveStolen.verificationEvidence!.commands[0]!.error = "stale host exception"; + const interruptedImplement = liveStolen.assignments.find( + (assignment) => assignment.role === "implement", + ); + assert.ok(interruptedImplement); + interruptedImplement.status = "interrupted"; liveStolen.lease = { processId: 200, activeParentSessionId: "transient-session", diff --git a/extensions/pi-workflow/engine.ts b/extensions/pi-workflow/engine.ts index 09e2cd9..45894cd 100644 --- a/extensions/pi-workflow/engine.ts +++ b/extensions/pi-workflow/engine.ts @@ -4348,16 +4348,17 @@ export class WorkflowEngine { next.status = "running"; }); if (run.implementationContext?.status === "ready") { - if (interruptedMutating.length > 0) { + if ( + interruptedMutating.length > 0 && + stage(run, "implement").status !== "completed" + ) { run = await this.transition(run, (next) => { const implement = stage(next, "implement"); - if (implement.status !== "completed") { - implement.status = "completed"; - implement.finishedAt = this.now(); - implement.output = { - recovery: "User requested a new remediation Assignment after interruption", - }; - } + implement.status = "completed"; + implement.finishedAt = this.now(); + implement.output = { + recovery: "User requested a new remediation Assignment after interruption", + }; }); return this.remediate(run); }