From 802a4249fbb40941582f04a57e1c7ffa68c629b4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 15 Aug 2026 17:13:20 -0700 Subject: [PATCH] FN-9106: Route non-continuable step sessions through retry recovery Recover poisoned step-session transcripts before the generic failure sink can retain them. - Apply bounded fresh-session recovery to incomplete step-session execution failures. - Preserve completed-work handling ahead of retry classification. - Add regression coverage and production-shaped executor fixtures. - Document the published patch behavior in a changeset. Files changed: .changeset/fn-9106-non-continuable-step-session.md | 7 ++++ .../post-done-continuation-no-wedge.test.ts | 48 +++++++++++++++++++++- packages/engine/src/executor/run-implementation.ts | 11 +++++ 3 files changed, 65 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-9106 Fusion-Task-Lineage: 537ea415-96dd-4b66-ab5d-84dc4603388b Co-authored-by: Fusion (runfusion.ai) --- .../fn-9106-non-continuable-step-session.md | 7 +++ .../post-done-continuation-no-wedge.test.ts | 48 ++++++++++++++++++- .../engine/src/executor/run-implementation.ts | 11 +++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-9106-non-continuable-step-session.md diff --git a/.changeset/fn-9106-non-continuable-step-session.md b/.changeset/fn-9106-non-continuable-step-session.md new file mode 100644 index 0000000000..53435c2f57 --- /dev/null +++ b/.changeset/fn-9106-non-continuable-step-session.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Non-continuable agent sessions now recover cleanly in step-session runs instead of failing the task. +category: fix +dev: Pairs run-implementation.ts step-session error handling with handleNonContinuableSessionRetry. diff --git a/packages/engine/src/__tests__/reliability-interactions/post-done-continuation-no-wedge.test.ts b/packages/engine/src/__tests__/reliability-interactions/post-done-continuation-no-wedge.test.ts index f16957c698..0b99751853 100644 --- a/packages/engine/src/__tests__/reliability-interactions/post-done-continuation-no-wedge.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/post-done-continuation-no-wedge.test.ts @@ -81,6 +81,13 @@ function createStore(task: Task, settingsOverrides: Record = {} }); (emitter as any).appendAgentLog = vi.fn().mockResolvedValue(undefined); (emitter as any).getGoalStore = vi.fn().mockReturnValue({ listGoals: vi.fn().mockReturnValue([]) }); + /* + FNXC:EngineTests 2026-08-15-23:48: + Graph principal admission (FN-8764/FN-8821) reads store.getRootDir() before it creates a + session. Keep this fixture production-shaped so the test reaches non-continuable handling + instead of terminally parking at steps#0:step-execute on a missing store method. + */ + (emitter as any).getRootDir = vi.fn().mockReturnValue("/tmp/test"); (emitter as any).getFusionDir = vi.fn().mockReturnValue("/tmp/test/.fusion"); (emitter as any).clearStaleExecutionStartBranchReferences = vi.fn().mockReturnValue([]); (emitter as any).listWorkflowSteps = vi.fn().mockResolvedValue([]); @@ -169,6 +176,13 @@ function createSelfHealingStore(tasks: Task[], settingsOverrides: Record { audits.push(event); }); + /* + FNXC:EngineTests 2026-08-15-23:48: + Graph principal admission (FN-8764/FN-8821) reads store.getRootDir() before it creates a + session. Self-healing fixtures retain the same TaskStore shape to prevent an admission + exception from making executor-session coverage vacuous. + */ + (emitter as any).getRootDir = vi.fn().mockReturnValue("/tmp/test"); // FNXC:EngineTests 2026-07-17-06:30: graph tool-failure cursor reads getAgentLogCount at execute entry. (emitter as any).getAgentLogCount = vi.fn().mockResolvedValue(0); (emitter as any).getAgentLogs = vi.fn().mockResolvedValue([]); @@ -250,7 +264,13 @@ describe("FN-5866 reliability interactions: post-done continuation no wedge", () throw new Error("Cannot continue from message role: assistant"); }); - const executor = new TaskExecutor(store, "/tmp/test", { onComplete, onError, agentStore: { getAgent: vi.fn().mockResolvedValue(null) } as any }); + /* + FNXC:EngineTests 2026-08-15-23:48: + Let executor-test-helpers supply its routing agent store. Principal admission requires + agentStore.listAgents() before a step session exists; the legacy getAgent-only bag holds + this case at workflow-principal-role-pool-exhausted and makes the continuation assertion vacuous. + */ + const executor = new TaskExecutor(store, "/tmp/test", { onComplete, onError }); await executor.execute(task); // Pre-fix root cause: the post-done step-session catch in executor.ts marked @@ -298,6 +318,32 @@ describe("FN-5866 reliability interactions: post-done continuation no wedge", () expect((task.log ?? []).some((entry: any) => entry.action.includes("Detected stale assistant-continuation session — fresh-session retry"))).toBe(true); }); + it("requeues incomplete step-session work with a fresh session when the session is not continuable", async () => { + const task = makeTask({ + id: "FN-9106-STEP-SESSION-INCOMPLETE", + steps: [{ name: "Implement", status: "in-progress" as const }], + sessionFile: "/tmp/test/.fusion/sessions/FN-9106-STEP-SESSION-INCOMPLETE.json", + }); + const store = createStore(task, { runStepsInNewSessions: true }); + const onError = vi.fn(); + + mockExecuteAll.mockRejectedValue(new Error("Cannot continue from message role: assistant")); + + const executor = new TaskExecutor(store, "/tmp/test", { onError }); + await executor.execute(task); + + expect(task.column).toBe("todo"); + expect(task.status).toBeUndefined(); + expect(task.error).toBeUndefined(); + expect(task.sessionFile).toBeNull(); + expect(task.recoveryRetryCount).toBe(1); + expect(task.nextRecoveryAt).toEqual(expect.any(String)); + expect(store.moveTask).toHaveBeenCalledWith(task.id, "todo", { preserveResumeState: true }); + expect(store.handoffToReview).not.toHaveBeenCalled(); + expect(onError).not.toHaveBeenCalled(); + expect((task.log ?? []).some((entry: any) => entry.action.includes("Non-continuable session — fresh-session retry"))).toBe(true); + }); + it("self-heals already wedged post-done non-continuable failures back to clean in-review", async () => { const wedged = makeTask({ id: "FN-5889-WEDGED", diff --git a/packages/engine/src/executor/run-implementation.ts b/packages/engine/src/executor/run-implementation.ts index 10e4326129..9b9f829ad8 100644 --- a/packages/engine/src/executor/run-implementation.ts +++ b/packages/engine/src/executor/run-implementation.ts @@ -1571,6 +1571,17 @@ export async function runImplementation( if (await deps.handleNonContinuableSessionError(task, false, errorMessage)) { return; } + /* + FNXC:PostDoneContinuation 2026-08-15-23:48: + Step-session failures must classify non-continuable transcripts in the same order as + single-session execution: completed work wins first; incomplete work gets the bounded + fresh-session retry (clearing sessionFile with recovery backoff) before this generic + resume sink can retain the poisoned transcript. Exhaustion clears its counters and + deliberately falls through to the existing sink. + */ + if (await deps.handleNonContinuableSessionRetry(task, errorMessage)) { + return; + } executorLog.error(`✗ ${task.id} step-session execution failed:`, errorDetail); await deps.store.logEntry(task.id, `Step-session execution failed: ${errorMessage}`, errorStack ?? errorDetail, deps.getRunContextFor(task.id)); await deps.store.updateTask(task.id, { status: null, error: null });