fix: prevent stale planning notifications
This commit is contained in:
7
.changeset/fix-stale-planning-background-task.md
Normal file
7
.changeset/fix-stale-planning-background-task.md
Normal 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.
|
||||||
@@ -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 () => {
|
it("dismissSession calls cancelSubtaskBreakdown for subtask sessions", async () => {
|
||||||
mockFetchAiSessions.mockResolvedValueOnce([
|
mockFetchAiSessions.mockResolvedValueOnce([
|
||||||
makeSession({ id: "subtask-session", status: "generating", type: "subtask" }),
|
makeSession({ id: "subtask-session", status: "generating", type: "subtask" }),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions
|
|||||||
const [sessions, setSessions] = useState<AiSessionSummary[]>([]);
|
const [sessions, setSessions] = useState<AiSessionSummary[]>([]);
|
||||||
const sessionTimestampsRef = useRef<Map<string, number>>(new Map());
|
const sessionTimestampsRef = useRef<Map<string, number>>(new Map());
|
||||||
const dismissedSessionTimestampsRef = useRef<Map<string, number>>(new Map());
|
const dismissedSessionTimestampsRef = useRef<Map<string, number>>(new Map());
|
||||||
|
const lastAuthoritativeRefreshAtRef = useRef(0);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessions: syncedSessions,
|
sessions: syncedSessions,
|
||||||
@@ -83,6 +84,16 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions
|
|||||||
for (const session of filtered) {
|
for (const session of filtered) {
|
||||||
nextTimestampMap.set(session.id, parseTimestamp(session.updatedAt));
|
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;
|
sessionTimestampsRef.current = nextTimestampMap;
|
||||||
setSessions(filtered);
|
setSessions(filtered);
|
||||||
})
|
})
|
||||||
@@ -118,6 +129,10 @@ export function useBackgroundSessions(projectId?: string): UseBackgroundSessions
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nextById.has(syncState.sessionId) && incomingTimestamp <= lastAuthoritativeRefreshAtRef.current) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const dismissedTimestamp = dismissedSessionTimestampsRef.current.get(syncState.sessionId);
|
const dismissedTimestamp = dismissedSessionTimestampsRef.current.get(syncState.sessionId);
|
||||||
if (dismissedTimestamp !== undefined && incomingTimestamp <= dismissedTimestamp) {
|
if (dismissedTimestamp !== undefined && incomingTimestamp <= dismissedTimestamp) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user