feat(ai-chat): прервать агента и отправить отложенное сообщение сейчас (#198) #203

Closed
Ghost wants to merge 2 commits from feat/198-interrupt-agent-send-now into develop

Что это

Кнопка «Send now» у каждого отложенного (queued) сообщения AI-чата: прерывает работающего агента, сохраняя его частичный вывод, и немедленно отправляет именно это сообщение. На следующем ходу агент получает пометку, что его прервали, и трактует свой предыдущий (частичный) ответ как незавершённый.

Closes #198.

Как работает

[клиент] promoteToHead(queue, X) → flushOnAbort/interruptNextSend = true → stop()
   ↓ abort долетает до сервера
[сервер] onAbort → строка ассистента финализируется как 'aborted' (частичный вывод сохранён)
   ↓
[клиент] onFinish({isAbort}) → flushNext() шлёт X с body.interrupted=true
   ↓
[сервер] interrupted && предыдущий ход aborted/streaming → INTERRUPT_NOTE в системный промпт
[модель] видит: (частичный aborted-ответ) + (user: X) + «тебя прервали, ответ был неполным»

Частичный вывод уже сохранялся (onAbortaborted) и реплеился модели (findRecent не фильтрует по статусу) — этот путь не менялся. Добавлены только UI-кнопка, одноразовый флаг interrupted в теле запроса и условная заметка в системном промпте.

Изменения

Клиент

  • queue-helpers.ts — чистый promoteToHead() (+ юнит-тесты).
  • chat-thread.tsxsendNow(), ветка слива на намеренном abort в onFinish, одноразовый interrupted в prepareSendMessagesRequest, сброс стейл-флагов на старте хода, кнопка-иконка в списке очереди.
  • en-US / ru-RU — ключи Send now, Interrupt and send now.

Сервер

  • ai-chat.service.tsinterrupted в AiChatStreamBody; чистый shouldInjectInterruptNote() (гейтит по флагу и реально незавершённому предыдущему ходу — aborted/streaming); проброс в buildSystemPrompt.
  • ai-chat.prompt.tsINTERRUPT_NOTE внутри safety-сэндвича.
  • Юнит-тесты промпта и сервиса.

Ревью

Пройдено внутреннее ревью; найдено и исправлено 2 Major (повторное ревью — APPROVE):

  1. Утечка interrupt-флагов в гонке «чистое завершение хода в момент клика» → сброс ref на старте каждого хода.
  2. Отсутствие ключей i18n в en-US/ru-RU (для ru-RU давало mixed-language) → добавлены.

Проверка

  • Тесты: клиент 12/12 (vitest), сервер 70/70 (jest).
  • Typecheck: чисто по затронутым файлам (оставшиеся 2 клиентские ошибки — предсуществующий артефакт footnote-definition-view.tsx, ��е из этого PR).
  • Сервер ESLint — чисто; клиентский ESLint не запускается из-за предсуществующей проблемы упаковки @tanstack/eslint-plugin-query (падает на загрузке конфига, не связано с PR).

🤖 Generated with Claude Code

## Что это Кнопка **«Send now»** у каждого отложенного (queued) сообщения AI-чата: прерывает работающего агента, сохраняя его частичный вывод, и немедленно отправляет именно это сообщение. На следующем ходу агент получает пометку, что его прервали, и трактует свой предыдущий (частичный) ответ как незавершённый. Closes #198. ## Как работает ``` [клиент] promoteToHead(queue, X) → flushOnAbort/interruptNextSend = true → stop() ↓ abort долетает до сервера [сервер] onAbort → строка ассистента финализируется как 'aborted' (частичный вывод сохранён) ↓ [клиент] onFinish({isAbort}) → flushNext() шлёт X с body.interrupted=true ↓ [сервер] interrupted && предыдущий ход aborted/streaming → INTERRUPT_NOTE в системный промпт [модель] видит: (частичный aborted-ответ) + (user: X) + «тебя прервали, ответ был неполным» ``` Частичный вывод **уже** сохранялся (`onAbort` → `aborted`) и реплеился модели (`findRecent` не фильтрует по статусу) — этот путь не менялся. Добавлены только UI-кнопка, одноразовый флаг `interrupted` в теле запроса и условная заметка в системном промпте. ## Изменения **Клиент** - `queue-helpers.ts` — чистый `promoteToHead()` (+ юнит-тесты). - `chat-thread.tsx` — `sendNow()`, ветка слива на намеренном abort в `onFinish`, одноразовый `interrupted` в `prepareSendMessagesRequest`, сброс стейл-флагов на старте хода, кнопка-иконка в списке очереди. - `en-US` / `ru-RU` — ключи `Send now`, `Interrupt and send now`. **Сервер** - `ai-chat.service.ts` — `interrupted` в `AiChatStreamBody`; чистый `shouldInjectInterruptNote()` (гейтит по флагу **и** реально незавершённому предыдущему ходу — `aborted`/`streaming`); проброс в `buildSystemPrompt`. - `ai-chat.prompt.ts` — `INTERRUPT_NOTE` внутри safety-сэндвича. - Юнит-тесты промпта и сервиса. ## Ревью Пройдено внутреннее ревью; найдено и исправлено 2 Major (повторное ревью — APPROVE): 1. Утечка interrupt-флагов в гонке «чистое завершение хода в момент клика» → сброс ref на старте каждого хода. 2. Отсутствие ключей i18n в `en-US`/`ru-RU` (для ru-RU давало mixed-language) → добавлены. ## Проверка - Тесты: клиент 12/12 (vitest), сервер 70/70 (jest). - Typecheck: чисто по затронутым файлам (оставшиеся 2 клиентские ошибки — предсуществующий артефакт `footnote-definition-view.tsx`, ��е из этого PR). - Сервер ESLint — чисто; клиентский ESLint не запускается из-за предсуществующей проблемы упаковки `@tanstack/eslint-plugin-query` (падает на загрузке конфига, не связано с PR). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 2 commits 2026-06-26 00:05:20 +03:00
Removed six outdated markdown files from the `docs/backlog` and other docs directories that were no longer relevant to the project. This cleans up the repository and reduces clutter.
Add a "Send now" button to each queued AI-chat message that interrupts the
running agent and immediately resends that message, preserving the agent's
partial output and telling it on the next turn that it was interrupted.

Client:
- queue-helpers: new pure promoteToHead() (+ tests)
- chat-thread: sendNow() promotes the chosen message to the queue head and
  aborts; onFinish flushes the promoted head on the intentional abort; a
  one-shot `interrupted` flag rides that resend request; stale flags are
  cleared at every turn start to defuse a clean-finish/click race leak
- "Send now" action icon + en-US/ru-RU translations

Server:
- AiChatStreamBody.interrupted flag; shouldInjectInterruptNote() gates on the
  flag AND a genuinely-unfinished previous turn (aborted/streaming)
- buildSystemPrompt() appends INTERRUPT_NOTE inside the safety sandwich so the
  model treats its previous, partial reply as incomplete
- prompt + service unit tests

Partial-output persistence already existed (onAbort -> 'aborted', findRecent
replays regardless of status); that path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vvzvlad added the feature label 2026-06-26 00:29:52 +03:00
vvzvlad added 1 commit 2026-06-26 04:48:36 +03:00
Addresses the open findings from the #198 ("Interrupt agent / send now")
code review (posted on PR #200's thread).

- sendNow: branch on the live status read from a new `statusRef` (updated
  every render) instead of the closure-captured `isStreaming`. If the turn
  finished between render and click, stale-true `isStreaming` made sendNow
  arm flushOnAbortRef/interruptNextSendRef and call a no-op stop(), leaving
  the one-shot flags armed to leak into a later turn. Reading the live
  status takes the not-streaming branch (send immediately) instead.
  Dependency array trimmed from [isStreaming, setQueue, stop] to
  [setQueue, stop].
- queued-state comment: drop the incorrect "(onFinish does not fire then)"
  claim — onFinish fires on every terminal outcome, only a clean finish
  flushes the queue, and a deliberate "Send now" flushes the promoted head
  via the abort branch of onFinish.

The third review finding (missing i18n keys "Send now" / "Interrupt and
send now") was already resolved in f789be9c.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vvzvlad force-pushed feat/198-interrupt-agent-send-now from 82699f7a20 to f789be9c89 2026-06-26 04:57:46 +03:00 Compare
Owner

Code review — «Interrupt agent / send now» (#198)

Вердикт: 🔶 Request changes — функциональное ядро корректно (сервер уже валидирует interrupted, штатные пути без гонок, promoteToHead чист и покрыт тестами), но change вносит два объективных дефекта, которые стоит закрыть до мержа: незарегистрированные i18n-ключи и устаревший комментарий, противоречащий новому поведению. Критичных проблем нет.

Объём: дифф против develop (merge-base fbdb8aa1), 3 файла, +96/−13. Полное ревью — все 8 аспектов (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture).

Должно быть исправлено до мержа

  • [conventions] Зарегистрировать новые t()-строки в каталогах локалейapps/client/src/features/ai-chat/components/chat-thread.tsx:514 и :520
    Дифф добавляет два UI-лейбла — t("Interrupt and send now") и t("Send now") — но ни один из ключей не зарегистрирован: проверено grep'ом, оба отсутствуют во всех локалях, включая исходный en-US и активный ru-RU. Все соседние строки этого же компонента ("Remove queued message", "Response stopped.") присутствуют в en-US и ru-RU; extraction-тулинга (i18next-parser и т.п.) в проекте нет — каталоги ведутся вручную. Сейчас строки рендерятся только из-за фолбэка i18next на сам ключ и в других языках никогда не переведутся — это вносимый диффом долг.
    Fix: добавить ключи "Interrupt and send now" и "Send now" в apps/client/public/locales/en-US/translation.json (источник) и перевод в ru-RU/translation.json, как сделано для соседнего "Remove queued message".

  • [documentation] Обновить устаревший блок-комментарий к состоянию queuedchat-thread.tsx:185-187
    Комментарий гласит: «On Stop or error the queue is intentionally preserved (onFinish does not fire then)». Новый поток «Send now» делает обе части ложными: sendNow сам зовёт stop(), а новая ветка в onFinish (:285-291) на этом аборте делает flushNext() — то есть осознанный Stop теперь сбрасывает очередь, а не сохраняет. Оговорка «onFinish does not fire then» прямо противоречит соседнему комментарию :269-276 и новой логике, которая как раз опирается на срабатывание onFinish при аборте.
    Fix: убрать неверное «(onFinish does not fire then)» и уточнить: очередь сохраняется при обычном Stop/disconnect/error, но намеренный «Send now» (тоже зовущий stop()) специально флашит продвинутую в голову запись через abort-ветку onFinish.

  • [warning][stability] Защитить streaming-ветку sendNow от гонки с устаревшим isStreamingchat-thread.tsx:355-361
    sendNow ветвится по захваченному в замыкании isStreaming (deps [isStreaming, setQueue, stop]), без сверки с живым статусом стора. Узкое окно гонки: если ход завершился между рендером и кликом, stop() становится no-op (срабатывает только при streaming/submitted), onFinish повторно не вызывается, и flushOnAbortRef остаётся взведённым. Тогда следующий настоящий ручной Stop уйдёт в abort-ветку onFinish, подавит уведомление «stopped» и сделает flushNext() — автоматически отправит сообщение из очереди, которое пользователь отправлять не просил (нарушение инварианта «на Stop очередь сохраняется»). Утечка interruptNextSendRef при этом безвредна — сервер гейтит заметку. Предусловие маловероятно (клик ровно в субкадровом окне между завершением хода и ре-рендером), но дефект реальный, фикс дешёвый.
    Fix: в streaming-ветке sendNow сверяться с живым статусом (например, держать statusRef, обновляемый каждый рендер) и армировать рефы только при submitted/streaming; иначе уходить в не-streaming ветку.

Покрытие тестами

  • Покрыто: новый чистый хелпер promoteToHead — 3 юнит-теста (середина→голова, отсутствующий id, отсутствие мутации). Соответствует конвенции фичи «вынести чистое решение в utils/*.ts и протестировать».
  • Не покрыто (suggestion, не блокирует): компонентная логика — диспетчеризация sendNow, interrupt-ветка onFinish, one-shot-семантика флага interrupted. Эта часть сплетена с useChat (stop/onFinish/рефы), а проект осознанно не рендерит ChatThread в тестах (RTL-тестов компонента / моков useChat в apps/client нет) — оставить React-обвязку без тестов соответствует конвенции. Опционально: вынести решение терминального исхода onFinish в чистый хелпер (decideTerminalAction({ intentionalInterrupt, isAbort, isDisconnect, isError })) и покрыть 5 случаями.

🏛 Архитектура

Координация очереди + прерывания в chat-thread.tsx размазана: состояние очереди и управление раскиданы по transport-useMemo, onFinish и обработчикам, склеены 4 рефами; два новых one-shot-флага (flushOnAbortRef, interruptNextSendRef) ставятся вместе в sendNow, но читаются-и-сбрасываются в разных колбэках в разное время. Инвариант «флаги одноразовые» держится на комментариях, не на структуре — тот же класс «разбросанных рефов жизненного цикла», который команда уже вынесла в этой фиче (hooks/use-chat-session.ts + utils/thread-identity.ts через useReducer).

вынести хук useChatQueue* ({ queued, enqueue, removeQueued, sendNow, onTurnSettled } + consumeInterruptFlag()) (effort: medium). + со-локация рефов/флагов/решений в одном тестируемом юните по образцу useChatSession; one-shot становится внутренним делом хука; chat-thread.tsx уменьшается. − хук всё равно седлает границу useChat (нужны stop, актуальный sendMessage, хук в onFinish/prepareSendMessagesRequest).

Аспекты без замечаний

Security — LGTM (флаг interrupted независимо ревалидируется в shouldInjectInterruptNote: нота вставляется только если предыдущий ход реально aborted/streaming; нота — статическая константа, инъекции нет; эндпоинт под прежними guard'ами, секретов нет). Regressions — LGTM (новая ветка onFinish не срабатывает на обычных Stop/disconnect/error — flushOnAbortRef по умолчанию false; поля тела chatId/openPage/roleId/messages сохранены, interrupted не конфликтует). Simplification — LGTM (два рефа оправданы — читаются в разные моменты жизненного цикла; promoteToHead минимален).


## Code review — «Interrupt agent / send now» (#198) **Вердикт: 🔶 Request changes** — функциональное ядро корректно (сервер уже валидирует `interrupted`, штатные пути без гонок, `promoteToHead` чист и покрыт тестами), но change вносит два объективных дефекта, которые стоит закрыть до мержа: незарегистрированные i18n-ключи и устаревший комментарий, противоречащий новому поведению. Критичных проблем нет. Объём: дифф против `develop` (merge-base `fbdb8aa1`), 3 файла, +96/−13. Полное ревью — все 8 аспектов (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture). ### ⛔ Должно быть исправлено до мержа - **[conventions] Зарегистрировать новые `t()`-строки в каталогах локалей** — `apps/client/src/features/ai-chat/components/chat-thread.tsx:514` и `:520` Дифф добавляет два UI-лейбла — `t("Interrupt and send now")` и `t("Send now")` — но ни один из ключей не зарегистрирован: проверено grep'ом, оба отсутствуют **во всех** локалях, включая исходный `en-US` и активный `ru-RU`. Все соседние строки этого же компонента (`"Remove queued message"`, `"Response stopped."`) присутствуют в `en-US` и `ru-RU`; extraction-тулинга (i18next-parser и т.п.) в проекте нет — каталоги ведутся вручную. Сейчас строки рендерятся только из-за фолбэка i18next на сам ключ и в других языках никогда не переведутся — это вносимый диффом долг. **Fix:** добавить ключи `"Interrupt and send now"` и `"Send now"` в `apps/client/public/locales/en-US/translation.json` (источник) и перевод в `ru-RU/translation.json`, как сделано для соседнего `"Remove queued message"`. - **[documentation] Обновить устаревший блок-комментарий к состоянию `queued`** — `chat-thread.tsx:185-187` Комментарий гласит: «On Stop or error the queue is intentionally preserved (onFinish does not fire then)». Новый поток «Send now» делает обе части ложными: `sendNow` сам зовёт `stop()`, а новая ветка в `onFinish` (`:285-291`) на этом аборте делает `flushNext()` — то есть осознанный Stop теперь **сбрасывает** очередь, а не сохраняет. Оговорка «onFinish does not fire then» прямо противоречит соседнему комментарию `:269-276` и новой логике, которая как раз опирается на срабатывание `onFinish` при аборте. **Fix:** убрать неверное «(onFinish does not fire then)» и уточнить: очередь сохраняется при *обычном* Stop/disconnect/error, но намеренный «Send now» (тоже зовущий `stop()`) специально флашит продвинутую в голову запись через abort-ветку `onFinish`. - **[warning][stability] Защитить streaming-ветку `sendNow` от гонки с устаревшим `isStreaming`** — `chat-thread.tsx:355-361` `sendNow` ветвится по захваченному в замыкании `isStreaming` (deps `[isStreaming, setQueue, stop]`), без сверки с живым статусом стора. Узкое окно гонки: если ход завершился между рендером и кликом, `stop()` становится no-op (срабатывает только при `streaming`/`submitted`), `onFinish` повторно не вызывается, и `flushOnAbortRef` остаётся взведённым. Тогда следующий *настоящий* ручной Stop уйдёт в abort-ветку `onFinish`, подавит уведомление «stopped» и сделает `flushNext()` — автоматически отправит сообщение из очереди, которое пользователь отправлять не просил (нарушение инварианта «на Stop очередь сохраняется»). Утечка `interruptNextSendRef` при этом безвредна — сервер гейтит заметку. Предусловие маловероятно (клик ровно в субкадровом окне между завершением хода и ре-рендером), но дефект реальный, фикс дешёвый. **Fix:** в streaming-ветке `sendNow` сверяться с живым статусом (например, держать `statusRef`, обновляемый каждый рендер) и армировать рефы только при `submitted`/`streaming`; иначе уходить в не-streaming ветку. ### ✅ Покрытие тестами - **Покрыто:** новый чистый хелпер `promoteToHead` — 3 юнит-теста (середина→голова, отсутствующий id, отсутствие мутации). Соответствует конвенции фичи «вынести чистое решение в `utils/*.ts` и протестировать». - **Не покрыто (suggestion, не блокирует):** компонентная логика — диспетчеризация `sendNow`, interrupt-ветка `onFinish`, one-shot-семантика флага `interrupted`. Эта часть сплетена с `useChat` (`stop`/`onFinish`/рефы), а проект осознанно не рендерит `ChatThread` в тестах (RTL-тестов компонента / моков `useChat` в `apps/client` нет) — оставить React-обвязку без тестов соответствует конвенции. Опционально: вынести решение терминального исхода `onFinish` в чистый хелпер (`decideTerminalAction({ intentionalInterrupt, isAbort, isDisconnect, isError })`) и покрыть 5 случаями. ### 🏛 Архитектура Координация очереди + прерывания в `chat-thread.tsx` размазана: состояние очереди и управление раскиданы по transport-`useMemo`, `onFinish` и обработчикам, склеены 4 рефами; два новых one-shot-флага (`flushOnAbortRef`, `interruptNextSendRef`) ставятся вместе в `sendNow`, но читаются-и-сбрасываются в *разных* колбэках в *разное* время. Инвариант «флаги одноразовые» держится на комментариях, не на структуре — тот же класс «разбросанных рефов жизненного цикла», который команда уже вынесла в этой фиче (`hooks/use-chat-session.ts` + `utils/thread-identity.ts` через `useReducer`). вынести хук `useChatQueue`* (`{ queued, enqueue, removeQueued, sendNow, onTurnSettled }` + `consumeInterruptFlag()`) (effort: medium). + со-локация рефов/флагов/решений в одном тестируемом юните по образцу `useChatSession`; one-shot становится внутренним делом хука; `chat-thread.tsx` уменьшается. − хук всё равно седлает границу `useChat` (нужны `stop`, актуальный `sendMessage`, хук в `onFinish`/`prepareSendMessagesRequest`). ### Аспекты без замечаний **Security — LGTM** (флаг `interrupted` независимо ревалидируется в `shouldInjectInterruptNote`: нота вставляется только если предыдущий ход реально `aborted`/`streaming`; нота — статическая константа, инъекции нет; эндпоинт под прежними guard'ами, секретов нет). **Regressions — LGTM** (новая ветка `onFinish` не срабатывает на обычных Stop/disconnect/error — `flushOnAbortRef` по умолчанию `false`; поля тела `chatId`/`openPage`/`roleId`/`messages` сохранены, `interrupted` не конфликтует). **Simplification — LGTM** (два рефа оправданы — читаются в разные моменты жизненного цикла; `promoteToHead` минимален). ---
Ghost added 1 commit 2026-06-26 17:19:38 +03:00
Address review on #198 (interrupt agent / send now):
- sendNow now branches on the live useChat status (statusRef) instead of
  the closure-captured isStreaming. A turn can finish between render and
  click, where stop() is a no-op; arming flushOnAbortRef/interruptNextSendRef
  against that no-op would strand the flags and leak into a later, unrelated
  Stop (auto-sending a queued message the user did not ask to send).
- Correct the stale queue comment: onFinish DOES fire on Stop/disconnect/
  error (its abort/disconnect/error branches leave the queue intact), and a
  deliberate "Send now" flushes the promoted head via the abort branch.

i18n keys for "Send now"/"Interrupt and send now" were already registered in
en-US and ru-RU on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ghost closed this pull request 2026-06-26 17:39:56 +03:00
Owner

Code review (re-review) — PR #203: прерывание агента и немедленная отправка очередного сообщения (#198)

Вердикт: Approve. Оба прошлых блокера закрыты по существу: незарегистрированные ключи t() теперь есть в обоих локалях, устаревший комментарий переписан под новое поведение. Дельта — оборонительное уточнение (live-status через ref, one-shot-флаги прерывания) без новых блокеров.

Ре-ревью дельты f789be9c..5b146fd2 (1 файлов, +20/−5). Аспекты: security, stability, conventions, documentation, test-coverage (параллельные ревьюеры + judge).

Статус прошлых блокеров

  • Незарегистрированные ключи t("Interrupt and send now") / t("Send now") — закрыт. Ключи присутствуют в apps/client/public/locales/en-US/translation.json:1178-1179 ("Send now", "Interrupt and send now") и в apps/client/public/locales/ru-RU/translation.json:718-719 ("Отправить сейчас", "Прервать и отправить сейчас"); потребители — chat-thread.tsx:539 (tooltip) и :545 (aria-label).
  • Устаревший комментарий, противоречащий новому поведению — закрыт. Блок-комментарий над queued переписан: теперь явно сказано, что onFinish ДЕЙСТВИТЕЛЬНО срабатывает на abort/disconnect/error, но оставляет очередь нетронутой, и описано единственное исключение — «Send now», которое флашит промотированную голову (chat-thread.tsx, дельта строк ~182–190).

Must fix before merge

Нет.

Non-blocking

  • [stability] (info)残ный edge: armed-флаг без последующего abortchat-thread.tsx:382 (sendNow) + :389-400 (useEffect). При истинной гонке (turn завершается чисто в том же тике, что и клик) flushOnAbortRef/interruptNextSendRef не потребляются ветками abort/send. Уже обезврежено: guard intentionalInterrupt && isAbort в onFinish и сброс обоих ref в useEffect(isStreaming). Действий не требуется — отмечено для полноты.

Test coverage

Дельта — оборонительное уточнение уже покрытого поведения (переход с closure-isStreaming на statusRef, без изменения серверной поверхности). Полный PR несёт тесты на хелперы очереди и сервис (queue-helpers.test.ts, ai-chat.service.spec.ts, ai-chat.prompt.spec.ts). Сама ветка statusRef-branch в sendNow юнит-тестом не покрыта, но это чисто клиентская защитная развилка поверх протестированной логики flush/interrupt — не блокер. Покрыто на достаточном уровне.

## Code review (re-review) — PR #203: прерывание агента и немедленная отправка очередного сообщения (#198) **Вердикт: Approve.** Оба прошлых блокера закрыты по существу: незарегистрированные ключи `t()` теперь есть в обоих локалях, устаревший комментарий переписан под новое поведение. Дельта — оборонительное уточнение (live-status через ref, one-shot-флаги прерывания) без новых блокеров. _Ре-ревью дельты `f789be9c..5b146fd2` (1 файлов, +20/−5). Аспекты: security, stability, conventions, documentation, test-coverage (параллельные ревьюеры + judge)._ ### Статус прошлых блокеров - **Незарегистрированные ключи `t("Interrupt and send now")` / `t("Send now")`** — закрыт. Ключи присутствуют в `apps/client/public/locales/en-US/translation.json:1178-1179` (`"Send now"`, `"Interrupt and send now"`) и в `apps/client/public/locales/ru-RU/translation.json:718-719` (`"Отправить сейчас"`, `"Прервать и отправить сейчас"`); потребители — `chat-thread.tsx:539` (tooltip) и `:545` (aria-label). - **Устаревший комментарий, противоречащий новому поведению** — закрыт. Блок-комментарий над `queued` переписан: теперь явно сказано, что `onFinish` ДЕЙСТВИТЕЛЬНО срабатывает на abort/disconnect/error, но оставляет очередь нетронутой, и описано единственное исключение — «Send now», которое флашит промотированную голову (`chat-thread.tsx`, дельта строк ~182–190). ### Must fix before merge Нет. ### Non-blocking - **[stability] (info)残ный edge: armed-флаг без последующего abort** — `chat-thread.tsx:382` (`sendNow`) + `:389-400` (`useEffect`). При истинной гонке (turn завершается чисто в том же тике, что и клик) `flushOnAbortRef`/`interruptNextSendRef` не потребляются ветками abort/send. Уже обезврежено: guard `intentionalInterrupt && isAbort` в `onFinish` и сброс обоих ref в `useEffect(isStreaming)`. Действий не требуется — отмечено для полноты. ### Test coverage Дельта — оборонительное уточнение уже покрытого поведения (переход с closure-`isStreaming` на `statusRef`, без изменения серверной поверхности). Полный PR несёт тесты на хелперы очереди и сервис (`queue-helpers.test.ts`, `ai-chat.service.spec.ts`, `ai-chat.prompt.spec.ts`). Сама ветка `statusRef`-branch в `sendNow` юнит-тестом не покрыта, но это чисто клиентская защитная развилка поверх протестированной логики flush/interrupt — не блокер. Покрыто на достаточном уровне.

Pull request closed

Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#203