Clear stale failed status on benign todo pause-abort

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) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-20 19:47:56 -07:00
parent c8c76a2d1b
commit 2d327604ff
3 changed files with 50 additions and 1 deletions

View File

@@ -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.

View File

@@ -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 "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js"; import { TaskExecutor } from "../executor.js";
import { createMockStore, resetExecutorMocks } from "./executor-test-helpers.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); 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 () => { it("STILL parks a non-todo (in-review) pause-abort as operator-action failed", async () => {
const { store, task, executor } = makeHarness({ column: "in-review" }); const { store, task, executor } = makeHarness({ column: "in-review" });

View File

@@ -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`; const todoBenign = `Workflow graph run ended during ${pauseProvenance} with task re-queued to todo — benign, cleared for normal scheduling`;
executorLog.log(`${task.id}: ${todoBenign}`); executorLog.log(`${task.id}: ${todoBenign}`);
await this.store.logEntry(task.id, todoBenign, undefined, this.getRunContextFor(task.id)); 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); await this.persistTokenUsage(task.id);
return; return;
} }