feat(ai-chat): resumable SSE — клиент (#381 PR 2, переоткрыт на develop после стек-мёржа) #389

Merged
vvzvlad merged 2 commits from feat/381-resumable-sse-pr2 into develop 2026-07-06 16:29:05 +03:00
Collaborator

Summary

Клиентская часть #381 (PR 2) — переоткрываю на develop, потому что при стековом мёрже она НЕ доехала до develop. closes #381 (клиентская половина)

Что случилось: PR 1 (#386) вмержил ветку feat/381-resumable-sse-pr1 в develop в состоянии ДО PR 2 (10d5220f). PR 2 (#387) был на базе feat/381-resumable-sse-pr1 и при мёрже ушёл В ЭТУ ВЕТКУ, а не в develop. Итог: develop получил только серверную половину (реестр), а клиентская (resume-машинерия + удаление поллинга/латчей) осталась осиротевшей в ветке. Проверил: resume-helpers.ts в develop НЕТ, run-polling.ts в develop ЕЩЁ ЕСТЬ.

Фикс: ребейзнул feat/381-resumable-sse-pr2 на актуальный develop (PR 1 + #351 + #382 уже там) — коммиты PR 1 отвалились (уже в develop), остался ровно клиентский дельта. База этого PR теперь develop (не стек).

Содержимое (тот же, что заапрувленный #387)

resume-машинерия в chat-thread.tsx (attach ?expect=live&anchor, 204-обработчик, arm/restore асимметрия инв.9, порядок onFinish инв.7, F7 restart-survival, abort при unmount, anchor-orphan), degraded-poll в ai-chat-window.tsx, resume-helpers.ts, удаление run-polling/useAiChatRunQuery/getAiChatRun/латчей. Ровно тот код, что прошёл 2 раунда ревью на #387 (approved).

How verified (на ребейзнутом состоянии поверх нового develop)

  • vitest src/features/ai-chat34 файла / 304 теста зелёные.
  • grep-guard по удалённым символам — пусто.
  • Ребейз чистый (0 конфликтов) — клиентский слой не пересекается с серверным PR 1.

Checklist

  • клиентская половина #381 доставлена в develop (была осиротевшей после стек-мёржа)
  • код идентичен заапрувленному #387; tsc/vitest зелёные, grep-guard пуст
## Summary Клиентская часть #381 (PR 2) — переоткрываю на `develop`, потому что при стековом мёрже она НЕ доехала до develop. closes #381 (клиентская половина) **Что случилось:** PR 1 (#386) вмержил ветку `feat/381-resumable-sse-pr1` в develop в состоянии ДО PR 2 (`10d5220f`). PR 2 (#387) был на базе `feat/381-resumable-sse-pr1` и при мёрже ушёл В ЭТУ ВЕТКУ, а не в develop. Итог: develop получил только серверную половину (реестр), а клиентская (resume-машинерия + удаление поллинга/латчей) осталась осиротевшей в ветке. Проверил: `resume-helpers.ts` в develop НЕТ, `run-polling.ts` в develop ЕЩЁ ЕСТЬ. **Фикс:** ребейзнул `feat/381-resumable-sse-pr2` на актуальный develop (PR 1 + #351 + #382 уже там) — коммиты PR 1 отвалились (уже в develop), остался ровно клиентский дельта. База этого PR теперь **develop** (не стек). ## Содержимое (тот же, что заапрувленный #387) resume-машинерия в `chat-thread.tsx` (attach ?expect=live&anchor, 204-обработчик, arm/restore асимметрия инв.9, порядок onFinish инв.7, F7 restart-survival, abort при unmount, anchor-orphan), degraded-poll в `ai-chat-window.tsx`, `resume-helpers.ts`, удаление `run-polling`/`useAiChatRunQuery`/`getAiChatRun`/латчей. Ровно тот код, что прошёл 2 раунда ревью на #387 (approved). ## How verified (на ребейзнутом состоянии поверх нового develop) - `vitest src/features/ai-chat` — **34 файла / 304 теста зелёные**. - grep-guard по удалённым символам — пусто. - Ребейз чистый (0 конфликтов) — клиентский слой не пересекается с серверным PR 1. ## Checklist - [x] клиентская половина #381 доставлена в develop (была осиротевшей после стек-мёржа) - [x] код идентичен заапрувленному #387; tsc/vitest зелёные, grep-guard пуст
agent_coder added 2 commits 2026-07-06 16:28:01 +03:00
PR 2 из 2 (#381, фаза 1.5 #184). Все вкладки теперь на ОДНОМ транспорте:
любая подключается к рану через GET-attach (реплей кадров + живой хвост,
реестр из PR 1). Наблюдатель — обычный стример; Stop, точки, инвалидации
работают штатно. Двухпутёвый поллинг снапшотов и вся latch-механика F4/F5/F7
удалены.

- utils/resume-helpers.ts (новый): isStreamingTail / isSettledAssistantTail /
  seedRows / mergeById (дословный перенос mergeObservedMessage из run-polling).
- components/chat-thread.tsx: resume-машинерия (гейтинг по не-settled хвосту,
  strip streaming-хвоста + attach ?expect=live&anchor=<row id>, транспорт
  prepareReconnectToStreamRequest+fetch, 204-обработчик из 4 частей,
  reconcile+degraded-merge, recovery с АСИММЕТРИЕЙ arm-vs-restore — при
  isDisconnect с видимым контентом только arm, без restore-клоббера живого
  стрима (инв. 9), строгий порядок onFinish с ранним return до обеих веток
  отправки (инв. 7), «Send now» скрыт на resumed-ходе, Stop абортит attach).
- components/ai-chat-window.tsx: degraded-poll фолбэк вместо латчей — тупой
  таймер (2500ms, 10-мин кап, без проверок ошибок/хвоста; переживает рестарт
  сервера), гасится тредом через onResumeFallback(false).
- Удалено: run-polling.ts(+test), useAiChatRunQuery/AI_CHAT_RUN_RQ_KEY,
  getAiChatRun (stopRun оставлен), IAiChatRun/IAiChatRunResponse, латчи
  stoppingRun/localStreaming/observedRow/onStreamingChange, F7-эффект,
  observer-merge. Серверный POST /ai-chat/run не тронут.

Проверка: tsc (мои файлы чисты), vitest src/features/ai-chat 34 файла/301 тест
зелёные, grep-guard по удалённым символам пуст. Отдельное внутреннее ревью на
инварианты 7/8/9 + 204-null-safety — чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Do 1 [F7 regression]: транзиентный сбой attach больше не роняет строку и не
теряет ран. В transport fetch-wrapper `204 || !response.ok` оба зовут
onNoActiveStream (восстановить stripped-строку + invalidate + арм poll), и catch
зовёт его перед rethrow — раньше !ok/throw только сбрасывали флаг, и на 5xx/502/
network-blip in-progress ассистент-турн исчезал, durable-ран не отслеживался.
onNoActiveStream — суперсет (его часть-г всё ещё чистит флаг), идемпотентен.
Расширяет литеральный block-3 спеки (там был только сброс флага) — по ревью и
в согласии с интенцией окна «poll must survive a server restart».

Do 2 [stability]: attach-GET абортится при unmount + mount-гейтинг сайд-эффектов.
mountedRef: mount-эффект ре-армит true и в cleanup ставит false + abort
attachAbortRef; onNoActiveStream рано выходит на !mounted, onFinish-recovery
гейтится `wasResumed && mountedRef.current`. Снимает до-10-мин спурьёзный поллинг
+ чужую invalidateQueries + утёкший fetch на новооткрытом чате (и StrictMode
double-resume).

Do 3 [coherence]: anchor-mismatch не оставляет вечную dots-строку. Reconcile
после мержа хвоста мержит fresh-history версию stripped-строки, если её id !=
id хвоста — settl'ит осиротевшую streaming-A над раном B.

Тесты: F7 500 → restore+арм; F7 network-throw → restore+арм; unmount при pending
attach → abort + поздние колбэки не летят. vitest src/features/ai-chat 304
зелёных, grep-guard пуст.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-06 16:28:01 +03:00
agent_coder force-pushed feat/381-resumable-sse-pr2 from 53d367662b to 18eee98b7f 2026-07-06 16:28:02 +03:00 Compare
vvzvlad merged commit 40227bbf51 into develop 2026-07-06 16:29:05 +03:00
Collaborator

🤖 Авто-ревью · #389 (feat(ai-chat): resumable SSE — клиент, #381 PR 2, переоткрыт на develop), голова 18eee98b

Вердикт: PASS (review/approved). Это КЛИЕНТСКИЙ контент #387 (обе итерации), переоткрытый прямо на develop — при стековом мёрже #387 ушёл в ветку feat/381-resumable-sse-pr1, а не в develop, и осиротел. Контент я уже полностью отревьюил в #387 (round 1 CHANGES → round 2 PASS).

Проверено (ре-таргет одобренного контента)

  • Клиентский код БАЙТ-В-БАЙТ = одобренный #387 round-2 (53d36766): git diff 53d367662b 18eee98b7f -- apps/client/.../ai-chat/пусто. Ребейз на develop не изменил клиентские файлы (они не конфликтуют с серверными/конвертерными изменениями develop).
  • Round-2 фиксы на месте (8 маркеров в chat-thread.tsx): F7 restart-survival (onNoActiveStream на !ok/throw), unmount abort (mountedRef), pending byte-cap (pendingBytes), anchor-orphan settle (stripped.id !== tail.id). Ноль конфликт-маркеров.
  • Серверная половина (реестр #386) уже в develop (PR-1 вмёржен) — клиент консюмит GET runs/:chatId/stream, эндпоинт существует.
  • Гейт зелёный на develop-базе: client vitest src/features/ai-chat34 файла passed; client tsc0 ошибок.

Контент идентичен ранее одобренному #387 round-2 (F7/unmount/anchor фиксы верифицированы там адверсариально + mutation-тестами). Мержибельно — это и закрывает клиентскую половину #381.

Ре-таргет одобренного PR на правильную базу после стек-мёрж-ошибки; клиентские файлы диффом подтверждены идентичными #387 round-2, гейт зелёный.

## 🤖 Авто-ревью · #389 (feat(ai-chat): resumable SSE — клиент, #381 PR 2, переоткрыт на develop), голова `18eee98b` **Вердикт: PASS** ✅ (review/approved). Это КЛИЕНТСКИЙ контент #387 (обе итерации), переоткрытый прямо на `develop` — при стековом мёрже #387 ушёл в ветку `feat/381-resumable-sse-pr1`, а не в develop, и осиротел. Контент я уже полностью отревьюил в #387 (round 1 CHANGES → round 2 PASS). ### Проверено (ре-таргет одобренного контента) - **Клиентский код БАЙТ-В-БАЙТ = одобренный #387 round-2** (`53d36766`): `git diff 53d367662b 18eee98b7f -- apps/client/.../ai-chat/` — **пусто**. Ребейз на develop не изменил клиентские файлы (они не конфликтуют с серверными/конвертерными изменениями develop). - **Round-2 фиксы на месте** (8 маркеров в `chat-thread.tsx`): F7 restart-survival (`onNoActiveStream` на `!ok`/throw), unmount abort (`mountedRef`), pending byte-cap (`pendingBytes`), anchor-orphan settle (`stripped.id !== tail.id`). Ноль конфликт-маркеров. - **Серверная половина (реестр #386) уже в develop** (PR-1 вмёржен) — клиент консюмит `GET runs/:chatId/stream`, эндпоинт существует. - **Гейт зелёный на develop-базе:** client vitest `src/features/ai-chat` — **34 файла passed**; client `tsc` — **0 ошибок**. Контент идентичен ранее одобренному #387 round-2 (F7/unmount/anchor фиксы верифицированы там адверсариально + mutation-тестами). Мержибельно — это и закрывает клиентскую половину #381. <sub>Ре-таргет одобренного PR на правильную базу после стек-мёрж-ошибки; клиентские файлы диффом подтверждены идентичными #387 round-2, гейт зелёный.</sub> <!-- state:review reviewed_head=18eee98b7f round=retarget verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-06 16:31:38 +03:00
Sign in to join this conversation.