fix(FN-6648): treat completed in-review tasks as benign despite lingering non-user paused flag

The paused-after-completion graceful-exit path finalizes a fully completed task to in-review while leaving a non-user paused:true flag set (handoffToReview/applyInReviewEnterEffects clear status/blockedBy but not paused). handleGraphFailure's completion-finalized guards required paused!==true, so once the volatile completion markers were lost (execute() re-entry deletes completionFinalizedTaskIds; teardown overwrites provenance to hard-cancel) the trailing graph failure was misclassified as an operator-action pause abort and the completed task was parked status:failed (FN-6638 recurrence). Drop the paused!==true requirement from alreadyFinalizedToReview and suppressFinalizedCompletionAbort, and gate genuinePauseAbort's bare paused clause on the completion suppression. Genuine userPaused/global-pause/in-progress tasks are unaffected.

Fusion-Task-Id: FN-6648
This commit is contained in:
gsxdsm
2026-06-18 13:21:59 -07:00
parent 16b6e5decb
commit b6823af049
3 changed files with 60 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix completed tasks being parked failed in in-review with a spurious "engine abort during pause/resume — operator action required" error (FN-6648; recurrence of FN-6478/FN-6568/FN-6625/FN-6644/FN-6647). The paused-after-completion graceful-exit path finalizes a fully completed task to in-review while leaving a non-user `paused` flag set; `handleGraphFailure`'s completion-finalized guards required `paused !== true`, so the trailing graph failure was misclassified as an operator-action pause abort once the volatile completion markers were lost. The classifier now recognizes finalized completions regardless of a lingering non-user pause flag, while genuine user/global pauses and in-progress tasks are unaffected.

View File

@@ -1612,6 +1612,40 @@ describe("TaskExecutor bounded recovery retries", () => {
expectBenignAlreadyAdvanced(store, column);
});
it("treats a completed in-review row as benign even with a lingering NON-user paused flag (FN-6648)", async () => {
/*
FNXC:WorkflowLifecycle 2026-06-18-16:25:
FN-6648 (FN-6638 recurrence): the paused-after-completion graceful-exit
path finalizes a fully completed task to in-review while leaving a
NON-user `paused: true` flag set (handoffToReview/applyInReviewEnterEffects
clear status/blockedBy but never `paused`). Worst case: the volatile
completionFinalized marker is lost (execute re-entry) AND provenance is
overwritten to hard-cancel by teardown — only persisted evidence remains.
This must resolve benignly, NOT park the completed task as an
operator-action "engine abort during pause/resume" failure.
*/
const store = createMockStore();
const task = makeCompletedTask();
store.getTask.mockResolvedValue({
...task,
column: "in-review",
paused: true,
userPaused: false,
status: undefined,
error: null,
});
const executor = new TaskExecutor(store, "/tmp/test", {});
(executor as any).markPausedAborted("FN-001", "hard-cancel");
await (executor as any).handleGraphFailure(task, {
disposition: "failed",
outcome: "failure",
visitedNodeIds: ["execute"],
});
expectBenignAlreadyAdvanced(store);
});
it("preserves explicit user-pause parking even when durable completion state exists", async () => {
const store = createMockStore();
const task = makeCompletedTask();

View File

@@ -6468,7 +6468,18 @@ export class TaskExecutor {
&& live.status == null
&& live.error == null
&& live.userPaused !== true
&& live.paused !== true
// FNXC:WorkflowLifecycle 2026-06-18-16:20:
// FN-6648: do NOT require `paused !== true` here. The
// paused-after-completion graceful-exit path (executor ~8748/8194)
// finalizes a FULLY COMPLETED task to in-review while leaving a
// NON-user `paused: true` flag set — handoffToReview /
// applyInReviewEnterEffects clear status/blockedBy/overlapBlockedBy
// but never `paused`. Requiring `paused !== true` made this clean
// completion unrecognizable, so `genuinePauseAbort` parked it failed
// with the spurious "engine abort during pause/resume" error
// (FN-6638 recurrence). `userPaused`/global-pause are still excluded,
// and `persistedCompletedProgress` + `persistedCompletionFinalizeLog`
// + status/error == null keep this scoped to genuine completions.
&& abortProvenance !== "global-pause"
&& !mergeSeamAborted
&& persistedCompletionFinalizeLog,
@@ -6478,14 +6489,21 @@ export class TaskExecutor {
completionFinalized
&& live.column !== "in-progress"
&& !live.userPaused
&& live.paused !== true
// FN-6648: `paused !== true` intentionally dropped here too — the
// suppression is already gated on `completionFinalized` (completed
// steps + finalize-to-review evidence) plus userPaused/global-pause
// exclusions, so a lingering non-user post-completion pause flag must
// not defeat it. See alreadyFinalizedToReview note above.
&& abortProvenance !== "global-pause"
&& !mergeSeamAborted,
);
const genuinePauseAbort = Boolean(
live.userPaused
|| abortProvenance === "global-pause"
|| (live.paused && !mergeSeamAborted)
// FN-6648: gate the bare `paused` clause on the completion-finalize
// suppression so a completed task carrying a non-user post-completion
// pause flag is not parked as an operator-action failure.
|| (live.paused && !mergeSeamAborted && !suppressFinalizedCompletionAbort)
|| (pausedAborted && !mergeSeamAborted && !completionFinalizeAborted && !suppressFinalizedCompletionAbort),
);
if (genuinePauseAbort) {