fix(chat): don't let chatCompletedHandler wipe taskIds from a concurrent send

chatCompletedHandler runs fire-and-forget and suspends at the getChatList
await, then unconditionally set taskIds = null. A sendMessage that resolved
during that window had its freshly registered task ids erased, so the stop
button vanished and in-flight background tasks (title gen, follow-ups,
tags) became uncancellable.

Snapshot taskIds before the await and only clear it if no concurrent write
happened since. For that reference check to be reliable, register task ids
as a new array instead of pushing in place; the in-place push also never
triggered Svelte reactivity, so the stop UI could miss appended ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #25217
This commit is contained in:
Classic298 2026-06-04 01:41:59 +02:00
parent 1a97751e37
commit cef983eed4

View file

@ -1556,11 +1556,15 @@
const chatCompletedHandler = async (_chatId, modelId, responseMessageId, messages) => {
// Backend handles outlet filters and persistence inline.
// Just refresh the sidebar chat list.
const capturedTaskIds = taskIds;
if ($chatId == _chatId && !$temporaryChatEnabled) {
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
taskIds = null;
// Don't clobber task ids a concurrent sendMessage registered while we were suspended.
if (taskIds === capturedTaskIds) {
taskIds = null;
}
};
const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => {
@ -2529,11 +2533,8 @@
} else {
// Backend returns task_ids (multi-model) or task_id (single model)
const newTaskIds = res.task_ids ?? (res.task_id ? [res.task_id] : []);
if (taskIds) {
taskIds.push(...newTaskIds);
} else {
taskIds = newTaskIds;
}
// New array each write so the snapshot guard in chatCompletedHandler can detect it.
taskIds = [...(taskIds ?? []), ...newTaskIds];
// Backend returns chat_id for new chats — set store + URL.
// Only update if the user hasn't navigated to a different chat