15 KiB
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
- Мьютекс добавлен, но
autoReconnectEnabledпо-прежнему обычныйvar, читаемый вне него. - Предел попыток введён, а сбрасывается он только при успешном подключении — после суток офлайна хост не восстановится сам даже при вернувшейся сети. Проверить сброс при появлении сети.
connect()ждёт готовности, ноHostsViewModelпо-прежнему показывает «подключено» по старому пути. Проверить UI, а не только рантайм.cancel()убран совсем — при зависшем закрытии соединение не освобождается никогда.- Гонка
gatewayReadyDeferred«починена» увеличением таймаута. - Подписки вынесены из
computeIfAbsent, но появилось окно, в котором рантайм уже в карте, а события ещё не собираются — первые события теряются. Проверить именно этот интервал. - Флаг миграции ставится до фактического завершения — прерванная миграция больше не повторится.
ConnectivityManagerподписан, но callback не отписывается — утечка на каждом пересоздании.- Foreground service держится постоянно, «чтобы не переподключаться» — расход батареи и риск для публикации.
- Пункт 6 сделан кодером 1 в обход §Роли, без решения в отчёте.
- В отчёте
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
./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), ## Решение по фоновой работе, ## Вердикт оркестратора.