fix: guard all chat response paths against session-switch race condition

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) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-02 01:09:28 +00:00
parent 9462da8b80
commit b8189a1999

View File

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