From 53427cde2cbe93338c7eb86e1c16ffb7f36dc8f3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 19:15:20 -0700 Subject: [PATCH] fix: prevent stale planning notifications --- .../fix-stale-planning-background-task.md | 7 ++++ .../__tests__/useBackgroundSessions.test.ts | 34 +++++++++++++++++++ .../app/hooks/useBackgroundSessions.ts | 15 ++++++++ 3 files changed, 56 insertions(+) create mode 100644 .changeset/fix-stale-planning-background-task.md diff --git a/.changeset/fix-stale-planning-background-task.md b/.changeset/fix-stale-planning-background-task.md new file mode 100644 index 0000000000..5318ec316f --- /dev/null +++ b/.changeset/fix-stale-planning-background-task.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent stale Planning notifications from pointing to missing sessions. +category: fix +dev: Background task sync now defers to the authoritative server session list. diff --git a/packages/dashboard/app/hooks/__tests__/useBackgroundSessions.test.ts b/packages/dashboard/app/hooks/__tests__/useBackgroundSessions.test.ts index 8e53ee6648..cdee5ade7d 100644 --- a/packages/dashboard/app/hooks/__tests__/useBackgroundSessions.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useBackgroundSessions.test.ts @@ -553,6 +553,40 @@ describe("useBackgroundSessions", () => { }); }); + it("does not materialize an older cross-tab session after the authoritative list is empty", async () => { + const { result } = renderHook(() => useBackgroundSessions()); + const { result: syncResult } = renderHook(() => useAiSessionSync()); + + await waitFor(() => { + expect(mockFetchAiSessions).toHaveBeenCalledWith(undefined); + expect(result.current.sessions).toEqual([]); + }); + + /* + * FNXC:BackgroundTasks 2026-07-11-19:08: + * A sibling tab can respond after the server has removed its planning + * session. Background Tasks must then remain empty because Planning reads + * the authoritative server list and cannot resume that stale cache entry. + */ + act(() => { + syncResult.current.broadcastUpdate({ + sessionId: "late-stale-planning", + status: "awaiting_input", + needsInput: true, + type: "planning", + title: "Late stale planning session", + updatedAt: "2026-04-08T00:00:01.000Z", + timestamp: Date.parse("2026-04-08T00:00:01.000Z"), + }); + }); + + await waitFor(() => { + expect(result.current.sessions).toEqual([]); + expect(result.current.planningSessions).toEqual([]); + expect(result.current.needsInput).toBe(0); + }); + }); + it("dismissSession calls cancelSubtaskBreakdown for subtask sessions", async () => { mockFetchAiSessions.mockResolvedValueOnce([ makeSession({ id: "subtask-session", status: "generating", type: "subtask" }), diff --git a/packages/dashboard/app/hooks/useBackgroundSessions.ts b/packages/dashboard/app/hooks/useBackgroundSessions.ts index 02489b7f7d..3e848ad018 100644 --- a/packages/dashboard/app/hooks/useBackgroundSessions.ts +++ b/packages/dashboard/app/hooks/useBackgroundSessions.ts @@ -51,6 +51,7 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions const [sessions, setSessions] = useState([]); const sessionTimestampsRef = useRef>(new Map()); const dismissedSessionTimestampsRef = useRef>(new Map()); + const lastAuthoritativeRefreshAtRef = useRef(0); const { sessions: syncedSessions, @@ -83,6 +84,16 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions for (const session of filtered) { nextTimestampMap.set(session.id, parseTimestamp(session.updatedAt)); } + + /* + * FNXC:BackgroundTasks 2026-07-11-19:08: + * Planning must never advertise a cached cross-tab session that the + * server's current project-scoped list does not contain. A late sync + * response is only a latency aid, so suppress updates older than this + * successful authoritative refresh; newer updates may still represent + * a session created immediately after the list request. + */ + lastAuthoritativeRefreshAtRef.current = Date.now(); sessionTimestampsRef.current = nextTimestampMap; setSessions(filtered); }) @@ -118,6 +129,10 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions continue; } + if (!nextById.has(syncState.sessionId) && incomingTimestamp <= lastAuthoritativeRefreshAtRef.current) { + continue; + } + const dismissedTimestamp = dismissedSessionTimestampsRef.current.get(syncState.sessionId); if (dismissedTimestamp !== undefined && incomingTimestamp <= dismissedTimestamp) { continue;