From 2d327604ffb5415567f0dc248d7946bcc8f799b4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 20 Jun 2026 19:47:56 -0700 Subject: [PATCH] Clear stale failed status on benign todo pause-abort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pause/resume abort parked status:"failed" on an earlier non-todo observation stays dispatchable (scheduler filters on column+paused, not status, scheduler.ts:1288) and re-enters the FN-6782 benign-todo branch. That branch logged "benign" but left status:"failed"/error on the row, so the board kept showing it failed and the deferred failure notification fired (notification-service fire-time check sees status==="failed") — contradicting the benign log. recoverPausedAbortFailures that would clear it is suppressed during global/engine pause (self-healing.ts:8125), so the failure survived the pause window. Reconcile the row with the benign reclassification: clear status/error when the live row carries them, so the board agrees it's benign and the pending notification is suppressed at dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...n-todo-pause-abort-failure-notification.md | 5 +++ .../executor-paused-abort-todo-benign.test.ts | 32 ++++++++++++++++++- packages/engine/src/executor.ts | 14 ++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-benign-todo-pause-abort-failure-notification.md diff --git a/.changeset/fix-benign-todo-pause-abort-failure-notification.md b/.changeset/fix-benign-todo-pause-abort-failure-notification.md new file mode 100644 index 0000000000..3d0750ba28 --- /dev/null +++ b/.changeset/fix-benign-todo-pause-abort-failure-notification.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Clear the stale `failed` status when a pause/resume abort is reclassified as a benign todo re-queue, so the task no longer surfaces as failed on the board and the deferred failure notification is suppressed. Previously a pause-abort parked `status:"failed"` on an earlier non-todo observation stayed dispatchable (the scheduler filters on column+paused, not status), re-entered the benign-todo branch, and was logged benign while the row stayed failed — firing a contradictory failure alert during global pause when self-healing recovery was suppressed. diff --git a/packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts b/packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts index a78c07791e..111931254c 100644 --- a/packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts +++ b/packages/engine/src/__tests__/executor-paused-abort-todo-benign.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import "./executor-test-helpers.js"; import { TaskExecutor } from "../executor.js"; import { createMockStore, resetExecutorMocks } from "./executor-test-helpers.js"; @@ -84,6 +84,36 @@ describe("pause-abort benign requeue-to-todo (FN-6782)", () => { expect((executor as any).activeWorktrees.has(task.id)).toBe(false); }); + it("clears a stale failed status when reclassifying a todo pause-abort as benign (no lingering failure notification)", async () => { + // FNXC:WorkflowLifecycle a pause-abort parked status:"failed" on an earlier + // non-todo observation stays dispatchable (scheduler filters column+paused, + // not status) and re-enters this branch in todo. The benign reclassification + // must reconcile the row to status:null/error:null — otherwise the persisted + // failure survives, the board shows it failed, and the deferred failure + // notification fires despite the benign log. + const { store, task, executor } = makeHarness({ + column: "todo", + status: "failed", + error: "Workflow graph failure surfaced after paused engine abort during pause/resume", + }); + (executor as any).activeWorktrees.set(task.id, task.worktree); + + await invokeGraphFailure(executor, task); + + const clearedFailure = store.updateTask.mock.calls.some( + (call: unknown[]) => { + const patch = call[1] as { status?: unknown; error?: unknown } | undefined; + return patch?.status === null && patch?.error === null; + }, + ); + expect(clearedFailure).toBe(true); + const reParkedFailed = store.updateTask.mock.calls.some( + (call: unknown[]) => (call[1] as { status?: string } | undefined)?.status === "failed", + ); + expect(reParkedFailed).toBe(false); + expect(logText(store)).toContain("benign, cleared for normal scheduling"); + }); + it("STILL parks a non-todo (in-review) pause-abort as operator-action failed", async () => { const { store, task, executor } = makeHarness({ column: "in-review" }); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 9540362a02..9e9fb954d0 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -6701,6 +6701,20 @@ export class TaskExecutor { const todoBenign = `Workflow graph run ended during ${pauseProvenance} with task re-queued to todo — benign, cleared for normal scheduling`; executorLog.log(`${task.id}: ${todoBenign}`); await this.store.logEntry(task.id, todoBenign, undefined, this.getRunContextFor(task.id)); + // FNXC:WorkflowLifecycle 2026-06-20: reconcile a stale persisted + // failure with the benign reclassification. A pause-abort parked + // `status:"failed"` on an earlier non-todo observation stays + // dispatchable (scheduler.ts filters column+paused, NOT status) and + // re-enters this branch in `todo`; `recoverPausedAbortFailures` that + // would clear it is suppressed during global/engine pause + // (self-healing.ts). Leaving the row failed contradicts the benign + // log: the board shows it failed AND the deferred failure + // notification fires (notification-service fire-time check sees + // status === "failed"). Clear status/error here so the row matches + // the log and the pending notification is suppressed at dispatch. + if (live.status != null || live.error != null) { + await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id)); + } await this.persistTokenUsage(task.id); return; }