From 492d375e8f94ad2d543c3f71d2da66bc00e4dba4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 10:25:16 -0700 Subject: [PATCH] FN-8536: fix planning retry ownership after failure Release a settled retry token so a later planning error can start the next bounded attempt. - Clear only the matching retry owner before scheduling its successor. - Cover distinct stream errors after retry settlement. - Add a patch changeset for the recovery fix. Files changed: .changeset/fn-8536-planning-retry.md | 7 +++++++ packages/dashboard/app/components/PlanningModeModal.tsx | 10 ++++++++++ .../__tests__/PlanningModeModal.planning-flow.test.tsx | 5 +++++ 3 files changed, 22 insertions(+) Fusion-Task-Id: FN-8536 Fusion-Task-Lineage: cccb0793-390e-4bdb-b459-939760e644bb Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8536-planning-retry.md | 7 +++++++ .../dashboard/app/components/PlanningModeModal.tsx | 10 ++++++++++ .../__tests__/PlanningModeModal.planning-flow.test.tsx | 5 +++++ 3 files changed, 22 insertions(+) create mode 100644 .changeset/fn-8536-planning-retry.md diff --git a/.changeset/fn-8536-planning-retry.md b/.changeset/fn-8536-planning-retry.md new file mode 100644 index 0000000000..1139361002 --- /dev/null +++ b/.changeset/fn-8536-planning-retry.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Planning Mode recovery retries safely bounded after failed attempts. +category: fix +dev: Releases the settled automatic retry owner before scheduling its token-guarded successor. diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 93b985f247..c1ed11fb32 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -1421,6 +1421,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat viewRef.current = { type: "loading" }; setView({ type: "loading" }); setIsAutoRetrying(true); + /* + FNXC:PlanningRetry 2026-07-23-09:45: + A rejected automatic attempt has settled before its bounded successor is queued, so + release only its matching token first. The successor can then acquire ownership, while + duplicate SSE/poll reports still coalesce only during a genuinely pending invocation; + a stale callback cannot clear a newer session's owner. + */ + if (options.retryToken && planningAutoRetryOwnerRef.current?.token === options.retryToken) { + planningAutoRetryOwnerRef.current = null; + } queueMicrotask(() => { if (currentSessionIdRef.current === retryTarget.sessionId) { void startPlanningAutoRetryRef.current(retryTarget.sessionId); diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx index 3a7ce5cf28..7896fe63da 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -166,6 +166,11 @@ describe("PlanningModeModal sequential flow", () => { resolveRetry({ success: true }); await Promise.resolve(); }); + + // Settlement releases the first owner. A later, distinct error may acquire the next + // bounded attempt; only the duplicate report while the promise was pending is coalesced. + mockConnectPlanningStream.mock.calls.at(-1)?.[2]?.onError?.("Later stream error"); + await waitFor(() => expect(mockRetryPlanningSession).toHaveBeenCalledTimes(2)); }); it("ignores a stale errored load after a newer session is selected", async () => {