From a79a6aaa513f422ce33e777a03b0da3c1c57bd0a Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 13 Aug 2026 16:15:24 +0800 Subject: [PATCH] fix(web): abort the stream when its in-flight question is deleted (#18214) --- web/src/pages/next-chats/chat-stream/store.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/web/src/pages/next-chats/chat-stream/store.ts b/web/src/pages/next-chats/chat-stream/store.ts index 0ecb403ed2..6420e6d07e 100644 --- a/web/src/pages/next-chats/chat-stream/store.ts +++ b/web/src/pages/next-chats/chat-stream/store.ts @@ -240,6 +240,12 @@ export const useChatStreamStore = create()( (state) => { const previous = state.sessions[conversationId]; if (!previous) return state; + // A session that is no longer streaming has no trailing placeholder + // to fill in. The reader's final flush runs before endStream, so + // this also guards the case where the in-flight question (and its + // placeholder) were just deleted: without it, the flush would slice + // off whatever message is now last. + if (!previous.isStreaming) return state; return { sessions: { ...state.sessions, @@ -304,6 +310,20 @@ export const useChatStreamStore = create()( removeMessageById: (conversationId, messageId) => { if (!conversationId) return; + + // Deleting the question that is currently being answered has to stop + // that answer too: the stream lives at module level, so it would + // otherwise keep producing tokens for a question that is gone. The + // question and its assistant placeholder are appended as a pair (and + // share an id), so the in-flight question sits second to last. + const session = get().sessions[conversationId]; + const removingStreamedQuestion = Boolean( + session?.isStreaming && session.messages.at(-2)?.id === messageId, + ); + if (removingStreamedQuestion) { + session?.abortController?.abort(); + } + set( (state) => { const previous = state.sessions[conversationId]; @@ -314,6 +334,15 @@ export const useChatStreamStore = create()( [conversationId]: { ...previous, messages: previous.messages.filter((x) => x.id !== messageId), + // Close the session right here instead of waiting for the + // reader's endStream, so the abort's trailing flush can't + // write the deleted answer back into the list. + isStreaming: removingStreamedQuestion + ? false + : previous.isStreaming, + abortController: removingStreamedQuestion + ? undefined + : previous.abortController, }, }, };