fix(client): вход в reconnect ждёт getRun без таймаута — hang залипает FSM в streaming (долг ревью #518) #541

Open
opened 2026-07-12 04:18:32 +03:00 by agent_vscode · 0 comments
Collaborator

Незакрытый WARNING из независимого ревью #518 (итерация 6 эпика #497); PR смержен в develop (#540) без этого фикса — переоформлено в ишью.

Суть. apps/client/src/features/ai-chat/components/chat-thread.tsx, onFinish/disconnect-ветка (~:750): вход в reconnect-лестницу происходит ТОЛЬКО после разрешения getRun(cid) (re-seed из персиста — норматив верный). Reject-путь обработан хорошо (dropLivePartialAndReplayFromStart + enterReconnect), но HANG не ограничен ничем: у axios-клиента (lib/api-client.ts) нет timeout, а stalled-idle-cap армится только ПОСЛЕ входа в reconnecting/polling, который при зависании не наступает.

Сценарий. Живой обрыв SSE + зависший (не упавший) getRun — соединение установлено, ответа нет → FSM залипает в streaming без баннера, без поллинга, до браузерного socket-timeout (минуты). Узко, но это новое связывание старта recovery с сетевым RTT в фиче про флаки-сеть — ровно класс «молчаливого замерзания», который эпик истребляет.

Правка. Promise.race(getRun(cid), timeout ~4–5 c); по таймауту — фолбэк на существующий reject-путь (dropLivePartialAndReplayFromStart + enterReconnect(runId)), поздний ответ getRun после таймаута игнорируется (или применяется идемпотентно через mergeById — решить при реализации, у обоих вариантов инварианты дублей соблюдаются). Альтернатива из ревью: входить в лестницу сразу, re-seed применять по приходу getRun.

Тест. Unit: getRun никогда не резолвится → через таймаут FSM в reconnecting, live-partial сброшен, replay-from-start; поздний resolve не ломает состояние (epoch/идемпотентность).

Заодно из follow-up'ов того же ревью (можно тем же PR): тест delta-обвязки окна (курсор-lifecycle + setQueryData-shape), ведущий tick поллинга (первые delta-строки к ~5 c), property-тест реконструкции по нормализованным транскриптам поверх trip-wire.

Незакрытый WARNING из независимого ревью #518 (итерация 6 эпика #497); PR смержен в develop (#540) без этого фикса — переоформлено в ишью. **Суть.** `apps/client/src/features/ai-chat/components/chat-thread.tsx`, onFinish/disconnect-ветка (~:750): вход в reconnect-лестницу происходит ТОЛЬКО после разрешения `getRun(cid)` (re-seed из персиста — норматив верный). Reject-путь обработан хорошо (`dropLivePartialAndReplayFromStart` + `enterReconnect`), но **HANG не ограничен ничем**: у axios-клиента (`lib/api-client.ts`) нет `timeout`, а stalled-idle-cap армится только ПОСЛЕ входа в `reconnecting`/`polling`, который при зависании не наступает. **Сценарий.** Живой обрыв SSE + зависший (не упавший) getRun — соединение установлено, ответа нет → FSM залипает в `streaming` без баннера, без поллинга, до браузерного socket-timeout (минуты). Узко, но это новое связывание старта recovery с сетевым RTT в фиче про флаки-сеть — ровно класс «молчаливого замерзания», который эпик истребляет. **Правка.** `Promise.race(getRun(cid), timeout ~4–5 c)`; по таймауту — фолбэк на существующий reject-путь (`dropLivePartialAndReplayFromStart` + `enterReconnect(runId)`), поздний ответ getRun после таймаута игнорируется (или применяется идемпотентно через mergeById — решить при реализации, у обоих вариантов инварианты дублей соблюдаются). Альтернатива из ревью: входить в лестницу сразу, re-seed применять по приходу getRun. **Тест.** Unit: getRun никогда не резолвится → через таймаут FSM в `reconnecting`, live-partial сброшен, replay-from-start; поздний resolve не ломает состояние (epoch/идемпотентность). Заодно из follow-up'ов того же ревью (можно тем же PR): тест delta-обвязки окна (курсор-lifecycle + setQueryData-shape), ведущий tick поллинга (первые delta-строки к ~5 c), property-тест реконструкции по нормализованным транскриптам поверх trip-wire.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#541