fix: prevent stale planning notifications

This commit is contained in:
gsxdsm
2026-07-11 19:15:20 -07:00
parent 79264d4990
commit 53427cde2c
3 changed files with 56 additions and 0 deletions

View File

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

View File

@@ -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" }),

View File

@@ -51,6 +51,7 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions
const [sessions, setSessions] = useState<AiSessionSummary[]>([]);
const sessionTimestampsRef = useRef<Map<string, number>>(new Map());
const dismissedSessionTimestampsRef = useRef<Map<string, number>>(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;