feat(ai-chat): interrupt agent + send queued message, keeping partial output (#198) #211
Reference in New Issue
Block a user
Delete Branch "feat/198-interrupt-agent"
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?
Closes #198
Lets the user interrupt a running agent and immediately send a queued message ("send now"), keeping the agent partial output.
Client
queue-helpers.ts: purepromoteToHead(+ unit tests).chat-thread.tsx:sendNow(promote head -> abort -> flush-on-abort), one-shotflushOnAbortRef/interruptNextSendRef,interruptedflag in the request body, and a "send now" ActionIcon next to the existing clock/cross in the queued list.Server
interrupted?: booleanonAiChatStreamBody; pureisInterruptResumeconfirms the client hint against persisted history (previous assistant turnaborted/streaming) before honouring it — a spoofed flag on an ordinary turn is ignored.ai-chat.prompt.ts:INTERRUPT_NOTEinjected into the context section (inside the safety sandwich) only on a confirmed interrupt-resume turn, so the model treats the partial answer above as incomplete. The partial output itself is already replayed from history (findRecent does not filter by status).Tests (unit only)
promoteToHead; chat-thread send-now (abort + resend on abort, one-shot interrupt flag, non-streaming immediate send).isInterruptResume(flag confirmation vs aborted/streaming/completed/non-assistant/no-prev); prompt interrupt-note present when interrupted, absent otherwise.Verify
pnpm --filter server exec tsc --noEmit: cleanpnpm --filter client exec tsc -b: cleansrc/core/ai-chat/: 346 passed🤖 Generated with Claude Code
Code review — PR #211: feat(ai-chat): прерывание агента и немедленная отправка сообщения из очереди (#198)
Вердикт: Request changes. Логика прерывания корректна и покрыта тестом, чистые хелперы (
promoteToHead,isInterruptResume) вынесены и протестированы. Но must-fix: два новых i18n-ключа не зарегистрированы ни в одном каталоге локалей (нарушение задокументированной политики «en-US и ru-RU полностью поддерживаются, UI никогда не рендерит смешанный язык»), и вai-chat.prompt.tsпоявился мёртвыйexportконстанты.Объём: дифф
develop…feat/198-interrupt-agent(merge-base3ddc329b), 8 файлов, +447/−13. Прогнаны параллельные аспектные ревьюеры (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture) + judge-проход.Must fix before merge
[i18n] Зарегистрировать ключи «Send now» и «Interrupt and send now» в каталогах en-US и ru-RU —
apps/client/src/features/ai-chat/components/chat-thread.tsx:521-529Два новых пользовательских ключа —
t("Interrupt and send now")(tooltip) иt("Send now")(aria-label) — отсутствуют во всех каталогах локалей (проверено grep'ом: ни в en-US, ни в ru-RU). Соседние ключи очереди («Remove queued message» → «Убрать из очереди») зарегистрированы в обоих, так что это новое отклонение, а не baseline. Политика вapps/client/src/i18n.ts:15-23явно требует держать набор строк AI-chat полным в обоих языках, «чтобы UI никогда не рендерил смешанный язык». Сейчас русский пользователь видит английские tooltip/aria-label рядом с уже переведённым рядом очереди — ровно тот mixed-language исход, который политика запрещает. Fix: добавить «Send now» и «Interrupt and send now» вapps/client/public/locales/en-US/translation.json(значение = английский текст ключа) и вru-RU/translation.jsonс русскими переводами, по образцу «Remove queued message».[simplification] Убрать неиспользуемый
export { INTERRUPT_NOTE }, оставить константу module-private —apps/server/src/core/ai-chat/ai-chat.prompt.ts:75Новый
export { INTERRUPT_NOTE };экспонирует константу, но её никто не импортирует (grep по репозиторию не находит ссылок вне файла; спекаai-chat.prompt.spec.tsнамеренно проверяет подстроку-маркер'interrupted by the', а не импортирует константу). Соседние константы того же модуля,DEFAULT_PROMPTиSAFETY_FRAMEWORK, специально module-private. Это change-introduced мёртвая публичная поверхность, противоречащая конвенции самого модуля. Fix: удалить строкуexport { INTERRUPT_NOTE };и объявитьconst INTERRUPT_NOTE = ...как private, по образцуDEFAULT_PROMPT/SAFETY_FRAMEWORK.[regressions] Сбрасывать
interruptNextSendRefв ветке flush-on-abort, когда отправлять нечего —apps/client/src/features/ai-chat/components/chat-thread.tsx:296-300, 363-373sendNow()взводитinterruptNextSendRef=trueи зовётstop(). Флаг read-and-clear выполняется только внутриprepareSendMessagesRequestпри реальной отправке. В ветке onFinish (flushOnAbortRefсбрасывается безусловно на :297), еслиflushNext()упирается вif (!head) return(пользователь удалил только что промоутнутое сообщение до приземления abort) — запрос не уходит, иinterruptNextSendRefостаётсяtrue, помечая СЛЕДУЮЩУЮ несвязанную отправку какinterrupted:true. Серверный guardisInterruptResumeнейтрализует видимый эффект в типовом случае, а предусловие надуманное — поэтому non-blocking, но залипший one-shot флаг остаётся латентной утечкой между ходами. Fix: в ветке flush-on-abort сбрасыватьinterruptNextSendRef.current=falseрядом сflushOnAbortRef(или безусловно там же), чтобы тег прерывания не пережил несвязанную последующую отправку.[documentation] Добавить запись в CHANGELOG
[Unreleased]/Addedпро фичу #198 —CHANGELOG.mdПроект ведёт Keep-a-Changelog с
## [Unreleased] / ### Added, обновляемым по-фично, и уже ссылается на номера issue соседних AI-chat фич (#143, #166, #168, #174, #180, #183). Эта PR поставляет пользовательскую фичу (прерывание агента + немедленная отправка из очереди, #198), но записи нет — #198 не встречается в файле. AGENTS.md:291 трактует CHANGELOG как release-time сборку из git log, поэтому это мягкая конвенция, не hard rule — non-blocking. Fix: добавить короткий### Addedбуллет под## [Unreleased]со ссылкой(#198), в стиле существующих AI-chat записей.Test coverage
Вся новая логика покрыта. Чистые хелперы (
promoteToHead,isInterruptResume) вынесены и юнит-тестированы; флоу прерывания end-to-end проверяется вchat-thread.test.tsx(мокаетuseChat, ai-transport и дочерние компоненты); серверная injectionINTERRUPT_NOTEпокрытаai-chat.prompt.spec.tsчерез подстроку-маркер. Замечание: ассертgetByLabelText("Send now")проходит только из-за raw-key fallback i18next, что маскирует пропуск ключей в каталогах (см. must-fix выше) — после регистрации ключей тест продолжит проходить.Architecture & design (forward-looking, non-blocking)
Координация in-mount очереди + «send now» в
ChatThread(apps/client/src/features/ai-chat/components/chat-thread.tsx). ChatThread (≈483 строк) разводит флоу прерывания по четырём несвязным точкам, сшитым мутабельными рефами:sendNow()промоутит цель и взводитflushOnAbortRef+interruptNextSendRef, затемstop(); transport-замыкание (prepareSendMessagesRequest) read-and-clear читаетinterruptNextSendRef; стабильныйonFinishчитаетflushOnAbortRefи ветвится вflushNext();flushNext()снова входит в transport. Итого пять координирующих рефов реализуют небольшую state-machine abort→promote→resend→tag прямо в компоненте, который ВДОБАВОК владеет жизненным цикломuseChat, ранним adoption chat-id, throttled live-token репортингом, stop-уведомлениями и рендером role-card. Чистые куски вынесены и протестированы; stateful-склейка — нет, и проверяется только через тяжёлый mock-everything тест компонента.Оставить inline как есть**, опционально добавить короткий ASCII-комментарий, называющий четыре точки касания state-machine (effort: s). Pros: нулевой поведенческий риск; флоу уже плотно прокомментирован, чистая логика уже вынесена/протестирована; избегает спекулятивной абстракции для фичи, которая может не вырасти. Cons: следующее поведение очереди/прерывания продолжит накапливать рефы и ветки onFinish в 483-строчном компоненте; stateful-склейка остаётся тестируемой только через mock-everything тест.
Ghost referenced this pull request2026-06-26 17:39:56 +03:00
Code review (re-review) — PR #211: прерывание AI-агента и отправка сообщения из очереди «сейчас»
Вердикт: Approve. Оба прошлых блокера закрыты по существу; дельта добавляет только защиту от утечки одноразового тега прерывания и не вносит новых проблем.
Ре-ревью дельты
50a96271..cbd877e1(5 файлов, +20/−4). Аспекты: stability, conventions, documentation, test-coverage (параллельные ревьюеры + judge).Статус прошлых блокеров
en-US/translation.jsonиru-RU/translation.json(с переводами «Отправить сейчас» / «Прервать и отправить сейчас») и реально используются вchat-thread.tsx:528,534черезt(...).export { INTERRUPT_NOTE }— закрыт. Экспорт удалён; константа осталась module-private и используется внутри файла (ai-chat.prompt.ts:191). Спека ссылается на неё только в комментарии, импорта нет — удаление безопасно, сборка/тесты не ломаются.Must fix before merge
Нет.
Non-blocking
Нет.
Test coverage
Покрыто по существу. Серверная логика инъекции INTERRUPT_NOTE покрыта
ai-chat.prompt.spec.ts:250(инъекция приinterrupted: true, отсутствие приfalse/absent). Единственная новая логика дельты —flushNext()возвращаетbooleanи при «пустом» dequeue (chat-thread.tsx:307) сбрасываетinterruptNextSendRef, чтобы одноразовый тег не утёк на следующую отправку; это узкий клиентский guard-путь, согласованный с существующим one-shot-сбросом тега вprepareSendMessagesRequest(строки 250–251), и его отсутствие в unit-тестах не является блокером.3fa0c67fc6to686c3f9d14