[bug][ai-chat] «New chat» во время стрима первого ответа не сбрасывает чат, а лишь убирает бейдж роли #161
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Симптом
Если создать чат со скиллом-ролью (например «Корректор») и, пока агент ещё
печатает первый ответ (статус
Thinking…), нажать New chat, то:очищается, новый пустой чат не открывается.
Реальный сброс к новому чату происходит только если нажать New chat после
того, как агент закончил ответ.
То есть «New chat» во время стрима первого хода работает как «снять бейдж роли»,
а не как «начать новый чат».
Воспроизведение
авто-стартует первый ход.
Thinking…), нажать New chat.Ветка на момент анализа:
develop@e9702434.Первопричина
Перемонтирование (сброс) треда чата управляется не самой кнопкой, а
reconciler-ом в фазе рендера внутри
useChatSession(
apps/client/src/features/ai-chat/hooks/use-chat-session.ts, ~стр. 187–195):Кнопка New chat (
apps/client/src/features/ai-chat/components/ai-chat-window.tsx,startNewChat, ~стр. 206–213) меняет только атомы:Ключевой момент: усыновление (adoption) реального id чата происходит только в
конце хода — в
onTurnFinished(вызывается изuseChat.onFinish). Пока идётстрим первого хода нового чата:
activeChatId === null(id ещё не усыновлён);thread.chatId === null.Поэтому
setActiveChatId(null)вstartNewChat— это no-op (значение и такnull), reconciler не срабатывает (null !== nullложно), тред неперемонтируется. Меняется только
selectedRoleId → null, из-за чего исчезаетбейдж роли (
currentRoleвai-chat-window.tsx, ~стр. 240–246).Почему «после конца работы агента» работает: к этому моменту adoption уже
выставил
activeChatId = <реальный id>(неnull). ТогдаsetActiveChatId(null)действительно меняет значение
id → null, reconciler видит расхождение иперемонтирует тред с новым ключом → открывается чистый новый чат.
Предлагаемая правка (дизайн)
1. Принудительный свежий тред
Перестать опираться на «изменение
activeChatId» как на единственный триггерперемонтирования. Добавить в
useChatSessionметод, который безусловносоздаёт новый тред (новый mount-key,
chatId: null), и вызывать его изstartNewChat:Так как новый ключ всегда отличается, React перемонтирует
ChatThread(новыйпустой
useChat), а render-phase reconciler уже не вмешивается, потому что последиспатча
activeChatId (null) === thread.chatId (null).2. Защита от «переусыновления» брошенного стрима (обязательно)
Здесь есть подводный камень. Хук
@ai-sdk/react(3.0.209) не прерывает стримпри размонтировании: у
useChatнет cleanup, который зовётstop(), аколлбеки проксируются через
callbacksRef, поэтомуonFinishстарого(брошенного) хода срабатывает даже после unmount и вызывает родительский
onTurnFinished(serverChatId). Поскольку после New chatactiveChatIdсноваnull, основной путь усыновления(
resolveAdoptedChatId(null, serverChatId)вadopt-chat-id.ts) усыновитброшенный чат и выдернет пользователя обратно в только что покинутый чат.
Решение — сделать усыновление «привязанным к треду»: передавать в
onTurnFinishedключ завершившегося треда и усыновлять/армировать fallbackтолько если он совпадает с текущим смонтированным
thread.key:ChatThreadполучает пропthreadKey(то же значение, что и Reactkey) ипрокидывает его в оба вызова
onTurnFinished(вonFinishиonError).useChatSessionдержатьthreadKeyRef(обновляется каждый рендер) и вonTurnFinished:isCurrentThread = finishingThreadKey === undefined || finishingThreadKey === threadKeyRef.current;isCurrentThread;isCurrentThread === false) делает лишьonInvalidateChatList()(чтобы покинутый чат появился в истории), безadoption и без
onInvalidateChatMessages.Это сохраняет «счастливый путь» (in-place adoption того же треда: ключ совпадает),
но не даёт фоновому завершению брошенного хода вернуть пользователя.
Затронутые файлы
apps/client/src/features/ai-chat/hooks/use-chat-session.ts—startFreshThread,threadKeyRef, thread-aware-гард вonTurnFinished, расширениеUseChatSessionResult.apps/client/src/features/ai-chat/components/ai-chat-window.tsx— вызовstartFreshThreadвstartNewChat, пробросthreadKeyвChatThread.apps/client/src/features/ai-chat/components/chat-thread.tsx— приём пропаthreadKeyи его проброс вonTurnFinished(вonFinishиonError).use-chat-session.test.tsx(новые кейсы: New chat во время стримаперемонтирует тред; брошенный тред не усыновляется), при необходимости
thread-identity.test.ts.Замечания / границы правки
на сервере в фоне» — это согласуется с текущим поведением переключения чатов на
лету (
selectChat), гдеuseChatтоже не прерывает запрос при unmount. Еслиотдельно потребуется реально останавливать фоновый ход при New chat (например,
чтобы «Корректор» перестал править документ после ухода) — это отдельная задача
про abort стрима при размонтировании.
threadKeyобратносовместим: если ключ не передан (undefined), вызовтрактуется как «текущий тред» — старые тесты/вызовы не ломаются.
claude_code referenced this issue2026-06-26 07:33:24 +03:00
claude_code referenced this issue2026-06-26 15:55:38 +03:00