fix(ai): cancel stale conversation restoration

Carry one selection generation across the complete session restoration flow so delayed list or history requests cannot override a newer restoration, user selection, or draft.
This commit is contained in:
digua committed 2026-08-22 18:42:22 +08:00
1 parent 982965b44a
commit 7d32e1d7ed
2 files changed
+53 -9

No files matched your search

+22 -7
View File
@@ -558,10 +558,13 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
return true
}
async function loadAIChat(chatKey: string, aiChatId: string): Promise<boolean> {
async function loadAIChatForSelection(
chatKey: string,
aiChatId: string,
selectionGeneration: number
): Promise<boolean> {
const state = getSessionState(chatKey)
if (!state) return false
const selectionGeneration = advanceAIChatSelection(chatKey)
if (!state || !isLatestAIChatSelection(chatKey, selectionGeneration)) return false
try {
const conversation = await useAIService().getAIChat(aiChatId)
@@ -599,6 +602,11 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
}
}
async function loadAIChat(chatKey: string, aiChatId: string): Promise<boolean> {
if (!getSessionState(chatKey)) return false
return loadAIChatForSelection(chatKey, aiChatId, advanceAIChatSelection(chatKey))
}
function focusAIChat(chatKey: string, aiChatId: string | null): boolean {
const state = getSessionState(chatKey)
if (!state) return false
@@ -635,24 +643,31 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
}
const state = getSessionState(chatKey)
if (!state || state.isAIThinking) return
const selectionGeneration = advanceAIChatSelection(chatKey)
if (!assistantStore.isLoaded) {
await assistantStore.loadAssistants()
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
}
if (preferredAIChatId && (await loadAIChat(chatKey, preferredAIChatId))) {
return
if (preferredAIChatId) {
if (await loadAIChatForSelection(chatKey, preferredAIChatId, selectionGeneration)) return
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
}
try {
const latestAIChat = (await useAIService().getAIChats(state.sessionId))[0]
if (latestAIChat && latestAIChat.id !== preferredAIChatId && (await loadAIChat(chatKey, latestAIChat.id))) {
return
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
if (latestAIChat && latestAIChat.id !== preferredAIChatId) {
if (await loadAIChatForSelection(chatKey, latestAIChat.id, selectionGeneration)) return
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
}
} catch (error) {
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
console.error('[AI] Failed to load the latest conversation:', error)
}
if (!isLatestAIChatSelection(chatKey, selectionGeneration)) return
if (!state.selectedAssistantId) {
const defaultId = getDefaultGeneralAssistantId(state.locale)
selectAssistantForSession(chatKey, defaultId)
+31 -2
View File
@@ -17,14 +17,27 @@ test('restores the latest valid AI chat without leaking stale navigation state',
const slowConversation = new Promise<ReturnType<typeof makeConversation>>((resolve) => {
resolveSlowConversation = resolve
})
let resolveStaleList!: (conversations: ReturnType<typeof makeConversation>[]) => void
const staleList = new Promise<ReturnType<typeof makeConversation>[]>((resolve) => {
resolveStaleList = resolve
})
let staleListRequestCount = 0
const aiService = {
getAIChat: async (id: string) => {
if (id === 'chat-slow') return slowConversation
if (id === 'chat-one' || id === 'chat-newest') return makeConversation(id)
if (id === 'chat-restored' || id === 'chat-user-choice') return makeConversation(id, 'session-four')
return id === 'chat-latest' ? makeConversation(id, 'session-three') : null
},
getAIChats: async (sessionId: string) =>
sessionId === 'session-three' ? [makeConversation('chat-latest', sessionId)] : [],
getAIChats: async (sessionId: string) => {
if (sessionId === 'session-three') return [makeConversation('chat-latest', sessionId)]
if (sessionId === 'session-four') {
staleListRequestCount++
if (staleListRequestCount === 1) return staleList
return [makeConversation('chat-restored', sessionId)]
}
return []
},
getMessages: async (aiChatId: string) => [
{
id: `message-${aiChatId}`,
@@ -120,6 +133,22 @@ test('restores the latest valid AI chat without leaking stale navigation state',
assert.equal(third.state.currentAIChatId, 'chat-latest')
assert.equal(third.state.messages[0]?.content, 'Saved answer for chat-latest')
const fourth = store.ensureSessionState({
sessionId: 'session-four',
sessionName: 'Fourth session',
chatType: 'private',
locale: 'zh-CN',
})
const staleRestore = store.resetToSelectorOnEnter(fourth.chatKey)
await store.resetToSelectorOnEnter(fourth.chatKey)
assert.equal(await store.loadAIChat(fourth.chatKey, 'chat-user-choice'), true)
resolveStaleList([makeConversation('chat-restored', 'session-four')])
await staleRestore
assert.equal(fourth.state.currentAIChatId, 'chat-user-choice')
assert.equal(fourth.state.messages[0]?.content, 'Saved answer for chat-user-choice')
const global = store.ensureGlobalState('zh-CN')
store.activeTask = {
requestId: 'global-request',