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_type: 'free_text',
|
||||||
intake_content: { text: prefill },
|
intake_content: { text: prefill },
|
||||||
})
|
})
|
||||||
|
const prefillChatId = session.session_id
|
||||||
|
currentChatRef.current = prefillChatId
|
||||||
const chatItem: ChatListItem = {
|
const chatItem: ChatListItem = {
|
||||||
id: session.session_id,
|
id: prefillChatId,
|
||||||
title: session.title,
|
title: session.title,
|
||||||
message_count: 0,
|
message_count: 0,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
@@ -165,28 +167,30 @@ export default function AssistantChatPage() {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(session.session_id)
|
setActiveChatId(prefillChatId)
|
||||||
setMessages([{ role: 'user', content: prefill }])
|
setMessages([{ role: 'user', content: prefill }])
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
const response = await aiSessionsApi.sendChatMessage(session.session_id, {
|
const response = await aiSessionsApi.sendChatMessage(prefillChatId, {
|
||||||
message: prefill,
|
message: prefill,
|
||||||
upload_ids: uploadIds?.length ? uploadIds : undefined,
|
upload_ids: uploadIds?.length ? uploadIds : undefined,
|
||||||
})
|
})
|
||||||
|
// Guard: discard if user switched sessions during the API call
|
||||||
|
if (currentChatRef.current !== prefillChatId) return
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
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, message_count: 2, title: prefill.slice(0, 100), updated_at: new Date().toISOString() }
|
||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Show task lane if AI sent questions or actions
|
// Show task lane if AI sent questions or actions
|
||||||
if (response.fork && session.session_id) {
|
if (response.fork && prefillChatId) {
|
||||||
branching.loadBranches(session.session_id)
|
branching.loadBranches(prefillChatId)
|
||||||
}
|
}
|
||||||
const hasQuestions = response.questions && response.questions.length > 0
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
const hasActions = response.actions && response.actions.length > 0
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
@@ -338,6 +342,7 @@ export default function AssistantChatPage() {
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || !activeChatId || loading) return
|
if (!input.trim() || !activeChatId || loading) return
|
||||||
|
|
||||||
|
const sendChatId = activeChatId
|
||||||
const userMessage = input.trim()
|
const userMessage = input.trim()
|
||||||
const completedUploadIds = pendingUploads
|
const completedUploadIds = pendingUploads
|
||||||
.filter((u) => u.status === 'done' && u.result?.id)
|
.filter((u) => u.status === 'done' && u.result?.id)
|
||||||
@@ -349,10 +354,13 @@ export default function AssistantChatPage() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await aiSessionsApi.sendChatMessage(activeChatId, {
|
const response = await aiSessionsApi.sendChatMessage(sendChatId, {
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined,
|
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' })
|
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -360,14 +368,14 @@ export default function AssistantChatPage() {
|
|||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
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, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() }
|
||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Load branches if fork was created
|
// Load branches if fork was created
|
||||||
if (response.fork && activeChatId) {
|
if (response.fork && sendChatId) {
|
||||||
branching.loadBranches(activeChatId)
|
branching.loadBranches(sendChatId)
|
||||||
}
|
}
|
||||||
// Show task lane if AI sent questions or actions
|
// Show task lane if AI sent questions or actions
|
||||||
const hasQuestions = response.questions && response.questions.length > 0
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
@@ -380,13 +388,16 @@ export default function AssistantChatPage() {
|
|||||||
// Merge triage update from AI
|
// Merge triage update from AI
|
||||||
if (response.triage_update) mergeTriageUpdate(response.triage_update)
|
if (response.triage_update) mergeTriageUpdate(response.triage_update)
|
||||||
} catch {
|
} catch {
|
||||||
|
if (currentChatRef.current !== sendChatId) return
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
{ role: 'assistant', content: 'Sorry, something went wrong. Please try again.' },
|
||||||
])
|
])
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (currentChatRef.current === sendChatId) {
|
||||||
requestAnimationFrame(() => inputRef.current?.focus())
|
setLoading(false)
|
||||||
|
requestAnimationFrame(() => inputRef.current?.focus())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,21 +441,24 @@ export default function AssistantChatPage() {
|
|||||||
setMessages([{ role: 'user', content: resumePrompt }])
|
setMessages([{ role: 'user', content: resumePrompt }])
|
||||||
setLoading(true)
|
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 => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
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, message_count: 2, title: resumePrompt.slice(0, 100), updated_at: new Date().toISOString() }
|
||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Show task lane if AI sent questions or actions
|
// Show task lane if AI sent questions or actions
|
||||||
if (response.fork && session.session_id) {
|
if (response.fork && resumeChatId) {
|
||||||
branching.loadBranches(session.session_id)
|
branching.loadBranches(resumeChatId)
|
||||||
}
|
}
|
||||||
const hasQuestions = response.questions && response.questions.length > 0
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
const hasActions = response.actions && response.actions.length > 0
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user