hermes-android/agy-work/TASK-2026-08-24-08-connection-lifecycle.md

122 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 пунктов 15 |
| Кодер 2 | Gemini Pro high | Независимая проверка, доводка, **и пункт 6** (фоновая работа) |
Пункт 6 отдан кодеру 2: foreground service затрагивает политику Play Store и разрешения, решение не механическое.
**Раунд 1 (кодер 1).** Тесты из §Required tests, фиксация падения на base SHA, затем код по пунктам 15.
**Раунд 2 (кодер 2).** Независимое воспроизведение, проход §Anti-checklist с явными отметками, доводка пунктов 15, затем пункт 6. Findings по шкале.
**Раунд 3 (оркестратор).** Приёмка при фактическом выводе команд и подтверждении поведения на устройстве для пунктов 56.
## Проблема
**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)`, `## Решение по фоновой работе`, `## Вердикт оркестратора`.