# Task 08: Жизненный цикл соединения, фон и сеть (hermes-android) **Repo:** `ochenstarik-ui/hermes-android` **Assigned to:** Antigravity (режим оркестратора, два кодера) **Priority:** HIGH (ложные статусы, бесконечные реконнекты, потеря работы при сворачивании) **Date:** 2026-08-24 **Base SHA:** результат задания 07 — указать фактический SHA при выдаче **Зависимость:** задание 01 принято — идентификация сокета и порядок событий уже исправлены; без этого поведение реконнекта не воспроизводимо. ## Роли и протокол | Роль | Модель | Что делает | |---|---|---| | Оркестратор | Antigravity | Разбивает работу, маршрутизирует, принимает результат | | Кодер 1 | Gemini Flash 3.7 high | Реализация §Scope пунктов 1–5 | | Кодер 2 | Gemini Pro high | Независимая проверка, доводка, **и пункт 6** (фоновая работа) | Пункт 6 отдан кодеру 2: foreground service затрагивает политику Play Store и разрешения, решение не механическое. **Раунд 1 (кодер 1).** Тесты из §Required tests, фиксация падения на base SHA, затем код по пунктам 1–5. **Раунд 2 (кодер 2).** Независимое воспроизведение, проход §Anti-checklist с явными отметками, доводка пунктов 1–5, затем пункт 6. Findings по шкале. **Раунд 3 (оркестратор).** Приёмка при фактическом выводе команд и подтверждении поведения на устройстве для пунктов 5–6. ## Проблема **1. Состояние реконнекта — незащищённые поля (`NET-05`).** `HermesHostRuntime.kt:52-54,231-245` — `autoReconnectEnabled`, `reconnectAttempt`, `reconnectJob` читаются и пишутся из нескольких корутин на `Dispatchers.Default` без `@Volatile` и мьютекса. Между проверкой `reconnectJob?.isActive` и присваиванием есть окно, в котором заводятся два параллельных цикла переподключения к одному хосту. Счётчик попыток растёт неограниченно, попытки не прекращаются никогда — даже когда хост заведомо недоступен. **2. `connect()` рапортует об успехе преждевременно (`NET-06`).** `HermesHostRuntime.kt:218-224` возвращает `Result.success(Unit)` сразу после `gatewayClient.connect()`, который лишь создаёт сокет. `HostsViewModel.kt:80-91` показывает пользователю «подключено» для соединения, которое может отвалиться через секунду. Ожидание `gateway.ready` есть только в `sendPrompt`. **3. `disconnect()` обрывает собственное закрытие (`NET-07`).** `JsonRpcGatewayClient.kt:154-166` — `close(1000, …)` и сразу `cancel()`: рукопожатие закрытия не успевает завершиться, сервер видит аварийный разрыв и дольше держит рантайм-сессию. **4. Гонка вокруг `gatewayReadyDeferred` (`NET-09`).** `JsonRpcGatewayClient.kt:73,93,139-144` — `connect()` заменяет объект целиком, а `awaitGatewayReady()` мог захватить ссылку на предыдущий и будет ждать его до таймаута при уже готовом соединении. **5. Служебные гонки менеджера (`DATA-11`, `DATA-12`).** `HermesConnectionManager.kt:91-110` запускает корутины внутри `ConcurrentHashMap.computeIfAbsent` — работа под блокировкой бина, любой повторный вход в карту даёт `IllegalStateException` или взаимоблокировку; `:150-153` — `refreshAllHosts` перезаписывает `_hosts`, не синхронизируя рантаймы. `MigrationHelper.kt:15-41` читает legacy DataStore при каждом холодном старте, флага завершения нет, legacy-блоб не удаляется, и миграция стартует параллельно менеджеру, который уже подписан на список хостов. **6. Нет реакции на сеть и фон (`UI-11`).** `ACCESS_NETWORK_STATE` запрошено в манифесте и не используется нигде. Foreground service и WorkManager отсутствуют: сворачивание приложения рвёт все WebSocket-соединения, длительная задача на хосте остаётся без клиента — при том что README обещает фоновое завершение работы и коммит результата в общий таймлайн. ## Scope **1. Управление реконнектом (`NET-05`).** Свести управление в один последовательный путь — `actor`/`Channel` либо `Mutex`; счётчик в `AtomicInteger`. Гарантировать: не более одного активного цикла реконнекта на хост. Ввести предел: после N неудачных попыток подряд хост переходит в состояние, требующее ручного действия, вместо вечного цикла. Значение N и поведение обосновать. Реконнект не запускать, когда сети нет вообще, — см. пункт 5. **2. Честный результат подключения (`NET-06`).** `connect()` возвращает успех после `awaitGatewayReady()`; таймаут — ошибка с различимой причиной. UI показывает «подключение» до готовности, а не «подключено». **3. Корректное закрытие (`NET-07`).** `close()`, ожидание `onClosed` с таймаутом, `cancel()` только если закрытие не завершилось. Проверить, что состояние по завершении — `Disconnected`, а не `Failed`. **4. Устранение гонки готовности (`NET-09`).** `gatewayReadyDeferred` читается один раз в локальную переменную под тем же примитивом, что и `activeWebSocket` (задание 01). Проверить сценарий: `awaitGatewayReady` вызван, следом `connect()` заменил соединение. **5. Служебные гонки и сеть (`DATA-11`, `DATA-12`).** Создание рантайма — внутри `computeIfAbsent`, подписки — после возврата из него. `refreshAllHosts` синхронизирует рантаймы тем же путём, что и основной сборщик в `init`. `MigrationHelper`: флаг завершения, очистка legacy-ключа, запуск до создания менеджера соединений. Подписка на `ConnectivityManager`: при потере сети соединения переводятся в понятное состояние и реконнект приостанавливается; при появлении — возобновляется немедленно, не дожидаясь очередного шага backoff. **6. Фоновая работа (`UI-11`) — кодер 2.** Сначала решение в отчёте: нужен ли foreground service, при каких условиях он стартует и когда останавливается, какой тип объявляется, что видит пользователь в уведомлении, как это соотносится с политикой Play Store по foreground-типам. Только затем код. Минимальное требование: пока хост выполняет задачу, инициированную пользователем, соединение живёт при свёрнутом приложении, и результат попадает в таймлайн. Когда активной задачи нет — сервис не держится. README привести в соответствие с фактическим поведением. ## Do not change - Идентификацию сокета и конвейер событий — задание 01, принято. - Схему БД и батчинг записи — задание 02; флаг миграции добавляется по правилам задания 02. - ViewModel и экраны — задание 03; здесь только состояние подключения, отображаемое в существующем UI. - Авторизацию и обновление токенов — задание 06. - Кэши в памяти и производительность списка — задание 09. ## Anti-checklist 1. Мьютекс добавлен, но `autoReconnectEnabled` по-прежнему обычный `var`, читаемый вне него. 2. Предел попыток введён, а сбрасывается он только при успешном подключении — после суток офлайна хост не восстановится сам даже при вернувшейся сети. Проверить сброс при появлении сети. 3. `connect()` ждёт готовности, но `HostsViewModel` по-прежнему показывает «подключено» по старому пути. Проверить UI, а не только рантайм. 4. `cancel()` убран совсем — при зависшем закрытии соединение не освобождается никогда. 5. Гонка `gatewayReadyDeferred` «починена» увеличением таймаута. 6. Подписки вынесены из `computeIfAbsent`, но появилось окно, в котором рантайм уже в карте, а события ещё не собираются — первые события теряются. Проверить именно этот интервал. 7. Флаг миграции ставится до фактического завершения — прерванная миграция больше не повторится. 8. `ConnectivityManager` подписан, но callback не отписывается — утечка на каждом пересоздании. 9. Foreground service держится постоянно, «чтобы не переподключаться» — расход батареи и риск для публикации. 10. Пункт 6 сделан кодером 1 в обход §Роли, без решения в отчёте. 11. В отчёте `green` для незапущенной команды (`AGENTS.md §3`). ## Definition of Done - Не более одного цикла реконнекта на хост при любом порядке событий подключения и отключения. - После предела неудачных попыток хост переходит в требующее действия состояние; появление сети восстанавливает работу. - «Подключено» показывается только после `gateway.ready`. - Штатное отключение завершается закрывающим рукопожатием; зависшее — освобождается по таймауту. - `awaitGatewayReady` не ждёт устаревший deferred. - Первые события после создания рантайма не теряются. - Миграция legacy выполняется однократно, до старта менеджера соединений. - Потеря и восстановление сети отражаются в состоянии хостов без вечного цикла попыток. - Задача, запущенная пользователем, доживает до конца при свёрнутом приложении, и результат попадает в таймлайн — подтверждено на устройстве. - Все существующие тесты зелёные, ни один не удалён и не ослаблен. ## Required tests `core/runtime/ReconnectSingleFlightTest.kt` — параллельные `Failed`/`Disconnected` дают один цикл реконнекта. Обязан падать на base SHA. `core/runtime/ReconnectLimitTest.kt` — предел попыток и сброс при появлении сети. `core/runtime/ConnectReadinessTest.kt` — `connect()` не возвращает успех до `gateway.ready`. Обязан падать на base SHA. `core/network/GracefulCloseTest.kt` — закрытие проходит рукопожатием; при отсутствии `onClosed` срабатывает таймаут. `core/network/ReadyDeferredRaceTest.kt` — `awaitGatewayReady` не залипает на устаревшем deferred. Обязан падать на base SHA. `core/runtime/RuntimeSubscriptionGapTest.kt` — событие, пришедшее сразу после создания рантайма, доставлено. `core/storage/MigrationOnceTest.kt` — повторный старт не повторяет миграцию; прерванная повторяется. Фоновая работа и поведение при переключении сети — на устройстве (`adb shell svc wifi disable/enable`, сворачивание, `dumpsys activity services`). Без устройства — `UNVERIFIED`. ## Required verification ```text ./gradlew --no-daemon testDebugUnitTest ./gradlew --no-daemon connectedDebugAndroidTest ./gradlew --no-daemon lint ./gradlew --no-daemon assembleDebug ``` ## Result `agents/antigravity/done/TASK-2026-08-24-08-connection-lifecycle.md` — разделы `## Кодер 1`, `## Кодер 2 (review + доработка + пункт 6)`, `## Решение по фоновой работе`, `## Вердикт оркестратора`.