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

15 KiB
Raw Blame History

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-245autoReconnectEnabled, 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-166close(1000, …) и сразу cancel(): рукопожатие закрытия не успевает завершиться, сервер видит аварийный разрыв и дольше держит рантайм-сессию.

4. Гонка вокруг gatewayReadyDeferred (NET-09). JsonRpcGatewayClient.kt:73,93,139-144connect() заменяет объект целиком, а awaitGatewayReady() мог захватить ссылку на предыдущий и будет ждать его до таймаута при уже готовом соединении.

5. Служебные гонки менеджера (DATA-11, DATA-12). HermesConnectionManager.kt:91-110 запускает корутины внутри ConcurrentHashMap.computeIfAbsent — работа под блокировкой бина, любой повторный вход в карту даёт IllegalStateException или взаимоблокировку; :150-153refreshAllHosts перезаписывает _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.ktconnect() не возвращает успех до gateway.ready. Обязан падать на base SHA. core/network/GracefulCloseTest.kt — закрытие проходит рукопожатием; при отсутствии onClosed срабатывает таймаут. core/network/ReadyDeferredRaceTest.ktawaitGatewayReady не залипает на устаревшем 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), ## Решение по фоновой работе, ## Вердикт оркестратора.