From b8189a19991f3a87d415da7e0c012b2a9af26ad0 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Thu, 2 Apr 2026 01:09:28 +0000 Subject: [PATCH] fix: guard all chat response paths against session-switch race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleSend, sendPrefill, and handleResumeNew all make async API calls that can return after the user has switched to a different session. Without a guard, the stale response overwrites the new session's questions/actions state — causing the previous session's FlowPilot Asks to persist. Fix: capture the session ID before each await and check currentChatRef after — discarding the response if the user has since switched. This matches the existing guard pattern in selectChat (lesson #106). Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/AssistantChatPage.tsx | 46 +++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index 819b524a..c173c7aa 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -156,8 +156,10 @@ export default function AssistantChatPage() { intake_type: 'free_text', intake_content: { text: prefill }, }) + const prefillChatId = session.session_id + currentChatRef.current = prefillChatId const chatItem: ChatListItem = { - id: session.session_id, + id: prefillChatId, title: session.title, message_count: 0, pinned: false, @@ -165,28 +167,30 @@ export default function AssistantChatPage() { updated_at: new Date().toISOString(), } setChats(prev => [chatItem, ...prev]) - setActiveChatId(session.session_id) + setActiveChatId(prefillChatId) setMessages([{ role: 'user', content: prefill }]) setLoading(true) - const response = await aiSessionsApi.sendChatMessage(session.session_id, { + const response = await aiSessionsApi.sendChatMessage(prefillChatId, { message: prefill, upload_ids: uploadIds?.length ? uploadIds : undefined, }) + // Guard: discard if user switched sessions during the API call + if (currentChatRef.current !== prefillChatId) return setMessages(prev => [ ...prev, { role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions }, ]) setChats(prev => prev.map(c => - c.id === session.session_id + c.id === prefillChatId ? { ...c, message_count: 2, title: prefill.slice(0, 100), updated_at: new Date().toISOString() } : c ) ) // Show task lane if AI sent questions or actions - if (response.fork && session.session_id) { - branching.loadBranches(session.session_id) + if (response.fork && prefillChatId) { + branching.loadBranches(prefillChatId) } const hasQuestions = response.questions && response.questions.length > 0 const hasActions = response.actions && response.actions.length > 0 @@ -338,6 +342,7 @@ export default function AssistantChatPage() { const handleSend = async () => { if (!input.trim() || !activeChatId || loading) return + const sendChatId = activeChatId const userMessage = input.trim() const completedUploadIds = pendingUploads .filter((u) => u.status === 'done' && u.result?.id) @@ -349,10 +354,13 @@ export default function AssistantChatPage() { setLoading(true) try { - const response = await aiSessionsApi.sendChatMessage(activeChatId, { + const response = await aiSessionsApi.sendChatMessage(sendChatId, { message: userMessage, upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined, }) + // Guard: if the user switched sessions while the API call was in flight, + // discard stale results to prevent overwriting the new session's state + if (currentChatRef.current !== sendChatId) return analytics.aiFeatureUsed({ feature: 'assistant_chat' }) setMessages(prev => [ ...prev, @@ -360,14 +368,14 @@ export default function AssistantChatPage() { ]) setChats(prev => prev.map(c => - c.id === activeChatId + c.id === sendChatId ? { ...c, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() } : c ) ) // Load branches if fork was created - if (response.fork && activeChatId) { - branching.loadBranches(activeChatId) + if (response.fork && sendChatId) { + branching.loadBranches(sendChatId) } // Show task lane if AI sent questions or actions const hasQuestions = response.questions && response.questions.length > 0 @@ -380,13 +388,16 @@ export default function AssistantChatPage() { // Merge triage update from AI if (response.triage_update) mergeTriageUpdate(response.triage_update) } catch { + if (currentChatRef.current !== sendChatId) return setMessages(prev => [ ...prev, { role: 'assistant', content: 'Sorry, something went wrong. Please try again.' }, ]) } finally { - setLoading(false) - requestAnimationFrame(() => inputRef.current?.focus()) + if (currentChatRef.current === sendChatId) { + setLoading(false) + requestAnimationFrame(() => inputRef.current?.focus()) + } } } @@ -430,21 +441,24 @@ export default function AssistantChatPage() { setMessages([{ role: 'user', content: resumePrompt }]) setLoading(true) - const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: resumePrompt }) + const resumeChatId = session.session_id + const response = await aiSessionsApi.sendChatMessage(resumeChatId, { message: resumePrompt }) + // Guard: discard if user switched sessions during the API call + if (currentChatRef.current !== resumeChatId) return setMessages(prev => [ ...prev, { role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions }, ]) setChats(prev => prev.map(c => - c.id === session.session_id + c.id === resumeChatId ? { ...c, message_count: 2, title: resumePrompt.slice(0, 100), updated_at: new Date().toISOString() } : c ) ) // Show task lane if AI sent questions or actions - if (response.fork && session.session_id) { - branching.loadBranches(session.session_id) + if (response.fork && resumeChatId) { + branching.loadBranches(resumeChatId) } const hasQuestions = response.questions && response.questions.length > 0 const hasActions = response.actions && response.actions.length > 0