# 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)`, `## Решение по фоновой работе`, `## Вердикт оркестратора`.
