1.7.9

Fixed (#408)

  • Вызовы инструментов во время live-хода больше не появляются выше только что отправленного сообщения пользователя. sortMessagesForRender ставил весь server-id блок перед optimistic-хвостом: пока user-строка не получила server id (echo message_persisted ещё в пути), карточка tool_batch_end с реальным id рендерилась выше неё; окно расширялось тем, что idle-актор персистил user-строку в handleInbound без echo (его публиковал только busy-путь persistUserMessageOnReceive), а ensureUserMessageSynced ранний return, когда optimistic user — хвост проекции. Исправление двумя осями. (1) Сервер: routeToActor выделяет per-ходовой turn_id (UUID, bus.MetaKeyTurnID) ДО персиста user-строки — его несут echo, thinking, все кадры streamHook (штамп в publish()), терминальные _turn_end и agent_status resume; idle-путь handleInbound публикует message_persisted echo для WS-строк с client_temp_id (зеркало busy-пути, паттерн #370) — оптимистичная user-строка свапается на server id сразу, а не после turn_end-merge. (2) Клиент: optimistic-строки получают sort_anchor = max server id на момент append — строка держит место выше server-строк, созданных позже в том же ходе (id монотонны), echo-свап не двигает позицию; строки без anchor (легаси/фикстуры) остаются хвостом — обратно совместимо, включая legacy tool_batch_end без message_id.

Fixed (#414)

  • Индикатор «печатает…» больше не остаётся в чате после финального ответа агента. Сброс busy-кластера был размазан по обработчикам с разными наборами полей: message_persisted и resetConnectionState (WS close) не сбрасывали isThinking вовсе, дубликаты/запоздалые терминалы предыдущего хода не отфильтровывались (нет корреляции), а sendAgentStatusResume молчал при busy=false — переподключившийся клиент не получал авторитетного idle и висел с устаревшим индикатором до нового сообщения/F5. Клиент получил единый идемпотентный терминальный переход finalizeLiveTurn (waitingForAgent, isThinking, agentPhase, стрим-буферы, active tools, skill runs, stop-frame, error-баннер; pendingAsk переживает терминал — вопрос живёт до ответа), в который сходятся все подтверждённые терминальные пути (turn_end, terminal message, непустой stream_end, agent_status busy=false, ask_user, unbound error, hard-watchdog, WS close). Guard от устаревших событий: activeTurnId принимается (one-shot) от «ход жив»-кадров (thinking/delta/user-echo/resume) и сравнивается с turn_id терминала — чужой/дубль терминал отбрасывается целиком; enterAwaitingTurn и resetState сбрасывают корреляцию и dedup-окно finalizedTexts. Сервер шлёт agent_status при каждом connect/attach ВСЕГДА (busy=true со снапшотом + turn_id, либо busy=false) — reconnect гарантированно приводит индикатор и композер в idle. Повторные терминалы одного хода (легитимные пары stream_end→turn_end, ask_user→turn_end) остаются рабочими: turn_end несёт свои токены/persist_failed/refetch вне finalize.

Review (#408, #414)

  • ocr review — исправлены: OnToolProgress публиковал в обход streamHook.publish (единственный несштампованный turn-scoped кадр), гонка currentTurnID со снапшотом reconnect (штамп до busy.Store(true) — GetSessionState гейтится по busy), finalizeLiveTurn не чистил this.error (баннер переживал ход на живом сокете), enterAwaitingTurn/resetState не сбрасывали finalizedTexts/activeTurnId, adoptTurnId перезаписывал корреляцию (one-shot-принятие — чужой thinking не может угнать id живого хода), wsCoalescablePair — ключ слияния delta-кадров дополнен TurnID (слабая уникальность stream_id допускает коллизию между ходами) + точечный тест. Отклонены с обоснованием: one-shot latch на повторные терминалы (сломал бы обязательный turn_end после stream_end — токены/persist_failed/refetch), мемоизация maxKnownServerId (окно чата ограничено, $derived-сорт уже O(n log n) на мутацию — кэш добавил бы пути инвалидации), перенос очистки subagent/extraction-остатка в finalize (memory-extraction сознательно живёт за границей хода), числовой ordinal вместо стабильной сортировки (стабильность Array.prototype.sort гарантирована ES2019+, прежний компаратор полагался на тот же контракт). Тесты: Go — turn id на эхе/в очереди актора/терминале/кадрах хука, idle-echo с tempID и без, always-send agent_status (idle/busy+turn_id), маппинг _turn_id→wire, коалесценция чужих ходов; vitest — live-порядок optimistic user против server-id tool-карточек (одиночный/несколько batch’ей/legacy/REST), задержанный echo без перестановки ленты, полная последовательность thinking → tool_batch_end → message_persisted → stream_end → turn_end с совпадением live/reload-порядка, терминальные переходы по каждому событию и их перестановкам/дублям, stale-терминал чужого хода, WS close/reconnect-resume, ask_user-исключение. Проверки: go test ./... -count=1 (0 падений), golangci-lint (0 новых; легаси-SA5011 в нетронутых файлах), go vet, npm run check (0), npm test (909), make build-cross (6 платформ) — зелёные.