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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user