# Task 01: Достижимость LAN-хоста и гонки в транспорте (hermes-android) **Repo:** `ochenstarik-ui/hermes-android` **Assigned to:** Antigravity (режим оркестратора, два кодера) **Priority:** CRITICAL (основной сценарий не работает + порча данных в стриме) **Date:** 2026-08-24 **Base SHA:** `ba5f0466f3fcb83fc2367ca61727ddb897529f88` ## Роли и протокол | Роль | Модель | Что делает | |---|---|---| | Оркестратор | Antigravity | Разбивает работу, маршрутизирует, принимает результат | | Кодер 1 | Gemini Flash 3.7 high | Реализация §Scope целиком | | Кодер 2 | Gemini Pro high | Независимая проверка работы кодера 1 и доработка | ### Раунд 1 — кодер 1 1. Сначала пишет тесты из §Required tests и **фиксирует их падение на base SHA**. Вывод падения — в отчёт дословно. 2. Только после этого правит код по §Scope, пункт за пунктом. 3. Архитектурных решений не принимает. Где в задании выбор не сделан явно — останавливается и записывает вопрос в раздел `OPEN QUESTIONS` своего отчёта, а не выбирает молча. 4. В отчёте: base SHA, список изменённых файлов, диф по каждому пункту §Scope, фактический вывод команд §Required verification. ### Раунд 2 — кодер 2 Работу кодера 1 **не переписывает целиком**. Порядок строго такой: 1. Независимо воспроизводит падение тестов на base SHA — своим запуском, не по отчёту кодера 1. 2. Читает диф против §Definition of Done и §Anti-checklist. Каждый пункт анти-чеклиста отмечает явно: `проверено — чисто` / `нарушено — <что именно>`. 3. Найденное исправляет сам, минимальным дифом поверх работы кодера 1. 4. Пишет findings по шкале `CRITICAL/HIGH/MEDIUM/LOW`. Если нарушений нет — пишет `findings: none` и перечисляет, что именно проверялось. Пустой раздел findings считается невыполненным ревью. Кодеру 2 запрещено: удалять или ослаблять тесты кодера 1; менять §DoD; расширять область задания; закрывать пункт формулировкой «выглядит корректно» без запуска. ### Раунд 3 — оркестратор Принимает результат только при выполнении всех условий: - оба отчёта содержат фактический вывод команд, а не утверждение «прошло»; - тесты из §Required tests падают на `ba5f046` и проходят после fix'а — подтверждено обоими кодерами независимо; - список изменённых файлов совпадает с фактическим дифом; - ни один существующий тест не удалён и не ослаблен; если тест всё же изменён — в отчёте построчное обоснование; - расхождения между отчётами кодеров разрешены до приёмки, а не задним числом. При расхождении выводов кодеров приоритет у того, кто приложил вывод команды. Никакой из кодеров не закрывает пункт по собственному заявлению. ## Проблема Аудит на `ba5f046` выявил четыре дефекта, которые блокируют базовую работу клиента. Первый ломает основной пользовательский сценарий целиком, остальные три портят поток сообщений и роняют экран чата. ### 1. Приложение физически не может соединиться с LAN-хостом `AndroidManifest.xml:17` объявляет `android:usesCleartextTraffic="false"`, файла `network_security_config.xml` в проекте нет. При этом весь онбординг построен на LAN-хосте: `hermes-pair` по умолчанию отдаёт в QR `scheme = "http"` (`hermes-pair/src/cli.rs`, `resolve_cli_endpoint`), `PairingPreviewDialog.kt:35` сам ставит галочку `allowCleartext` для http, `HostEntity.allowCleartext` хранится в БД, а `HermesRestClient.kt:37` и `JsonRpcGatewayClient.kt:86` проверяют этот флаг. Проверки в коде корректны — но до них дело не доходит: платформа отклоняет запрос к `http://192.168.x.x:9119` раньше, с `CLEARTEXT communication not permitted`. Пользовательский переключатель декоративен. Зеркальная половина: у LAN-хоста сертификат самоподписанный, trust anchors не настроены, поэтому вариант с `https://` тоже не соединяется. ### 2. Слушатели закрытых WebSocket-ов правят состояние живого соединения `JsonRpcGatewayClient.kt:107-136`: `connect()` перезаписывает `activeWebSocket`, но `WebSocketListener` предыдущего сокета остаётся зарегистрированным в OkHttp. Опоздавший `onFailure` от мёртвого сокета переводит `_connectionState` в `Failed` и обрывает `pendingRequests` уже установленного соединения, после чего `HermesHostRuntime.kt:83-88` запускает переподключение. Поле `private var activeWebSocket` (`:75`) пишется из потока диспетчера OkHttp и читается из корутин без `@Volatile`. ### 3. События могут прийти в обратном порядке Шаблон `if (!flow.tryEmit(e)) scope.launch { flow.emit(e) }` повторён на трёх хопах: `JsonRpcGatewayClient.kt:212-216` → `HermesHostRuntime.kt:60-62` → `HermesConnectionManager.kt:97-99`. При переполнении буфера событие уходит в отдельную корутину; буфер за это время освобождается, следующее событие проходит `tryEmit` мгновенно и обгоняет предыдущее. Для `message.delta` это перемешанные фрагменты текста в ответе. ### 4. `JsonNull` и пустые идентификаторы превращаются в валидные значения `GatewayEvents.kt:184-209`: `jsonPrimitive.content` на `JsonNull` возвращает строку `"null"` — поле `"session_id": null` даёт `sessionId = "null"`, по которому дальше идёт поиск в `runtimeToSessionMap`. Отсутствующий `message_id` даёт `""`, и эта строка уходит первичным ключом в Room (`UnifiedMessageEntity.id`) и ключом в `LazyColumn(key = { it.id })` (`ChatScreen.kt:241`). Два сообщения с пустым id — исключение Compose `Key was already used` и падение экрана чата. Дополнительно `.jsonPrimitive` бросает на поле-объекте, а `JsonRpcGatewayClient.kt:217-219` глушит это пустым `catch` — кадр исчезает бесследно, во всём приложении один вызов `Log`. ## Scope Ровно четыре пункта, в этом порядке. **1. Достижимость хоста (`SEC-01`).** Ввести `res/xml/network_security_config.xml` и подключить его в ``. Cleartext разрешён только там, где это осознанно нужно; для остального — запрещён. Явно указать в отчёте, какой атрибут выигрывает на `minSdk 26 … targetSdk 35` — `android:usesCleartextTraffic` или `networkSecurityConfig`, — со ссылкой на официальную документацию Android. Без подтверждённой ссылки пункт помечается `UNVERIFIED`, а не «сделано». Флаг `allowCleartext` из БД должен продолжать работать как второй уровень защиты в коде: разрешение на уровне платформы не отменяет проверок в `HermesRestClient.validateUrlScheme` и `JsonRpcGatewayClient.connect`. Половину про TLS с самоподписанным сертификатом в этом задании **не делать** — она вынесена в задание 06. Здесь только cleartext-путь. **2. Идентификация сокета (`NET-01`).** Первой строкой каждого колбэка `WebSocketListener` — отбрасывание событий не от текущего сокета. Поле `activeWebSocket` привести к потокобезопасному виду. Предыдущий сокет отменять явно перед созданием нового. Логику реконнекта в `HermesHostRuntime` в этом пункте не трогать. **3. Порядок событий (`NET-02`).** Убрать шаблон `tryEmit`-с-фолбэком **во всех трёх местах сразу**. Порядок доставки должен сохраняться при переполнении буфера. Допустимые решения — последовательный `Channel` с одним потребителем на хоп либо единственный эмиттер в одной корутине с `onBufferOverflow = SUSPEND`; выбранный вариант обосновать. Смешивать `tryEmit` и `emit` для одного потока нельзя. Проверить, что при этом не появилось блокировки потока OkHttp: `onMessage` не должен suspend'иться на переполненном буфере. **4. Валидация входящих событий (`NET-03`, `NET-04`).** Ввести безопасное чтение примитивов: `JsonNull` и не-примитив дают `null`, а не строку `"null"` и не исключение. Событие без обязательного идентификатора (`message_id` для message/thinking/reasoning, `tool_id` для tool, `request_id` для approval/clarify/sudo/secret) отбрасывается на границе парсера и **не доходит** до репозитория ни одним путём — включая ветки `MessageDeltaEvent` и `MessageCompleteEvent` в `UnifiedSessionRepository`, которые создают сообщение сами. Пустой `catch` в `handleIncomingMessage` заменить на обработку с логированием и счётчиком отброшенных кадров. Логгер не должен печатать значения токенов, тикетов, паролей и содержимого сообщений. ## Do not change - Контракт протокола: имена JSON-RPC методов, форму payload сопряжения, схему `hermes://pair` — только отдельным заданием. - Схему Room, `fallbackToDestructiveMigration`, порядок сообщений — задание 02. - ViewModel, навигацию, камеру, экраны — задание 03. - Мёртвый слой `feature/connections`, `feature/sessions`, `HermesGatewayRepository`, `ConnectionRepository` — задание 08. Здесь его не удалять и не чинить. - `hermes-pair/**` — задание 07. - Версии зависимостей, AGP, Kotlin, Compose BOM. - Тесты, не относящиеся к §Required tests, — не переписывать. ## Anti-checklist Кодер 2 обязан пройти каждый пункт и отметить результат явно. Это перечень способов сдать задание формально невыполненным при зелёных тестах. 1. Проверка `webSocket !== activeWebSocket` добавлена, но поле осталось обычным `var` — гонка сохранилась, тест зелёный случайно. 2. `tryEmit`-фолбэк убран в `JsonRpcGatewayClient`, но остался в `HermesHostRuntime` и/или `HermesConnectionManager`. Проверить все три файла поимённо. 3. Порядок событий «починен» переводом на `emit` прямо в `onMessage` — поток OkHttp теперь блокируется на переполненном буфере. Это регресс, а не фикс. 4. Валидация id добавлена в `GatewayEvent.parse`, но пустой id всё ещё попадает в Room и в ключ `LazyColumn` по другому пути. Проверить оба обработчика в репозитории, а не только парсер. 5. Тест написан на фейке, который не воспроизводит дефект (как `FakeUnifiedSessionDao`, скрывающий неопределённый порядок Room). Требование: каждый тест из §Required tests обязан падать на `ba5f046`. Не падает — тест не годится. 6. `network_security_config.xml` добавлен, но `android:usesCleartextTraffic="false"` в манифесте оставлен без разбора, какой из них применяется. Противоречивая пара — не «сделано». 7. В отчёте написано `green` / `PASS` для команды, которая фактически не запускалась из-за отсутствия JDK или Android SDK. Это прямое нарушение `AGENTS.md §3`. 8. Пустой `catch` заменён на логирование, которое печатает тело кадра целиком — то есть в logcat уезжают тикеты и содержимое переписки. ## Definition of Done - Запрос к хосту с `allowCleartext = true` и адресом `http://:` доходит до сети; запрос к хосту с `allowCleartext = false` по-прежнему отклоняется кодом клиента. - Опоздавший колбэк закрытого сокета не меняет `connectionState` и не обрывает `pendingRequests` актуального соединения. - Порядок событий на выходе `HermesConnectionManager.allEvents` совпадает с порядком на входе `handleIncomingMessage` при переполнении буфера на любом из трёх хопов. - Событие с `"session_id": null` не даёт `sessionId == "null"`. Событие без обязательного id не порождает ни строки в Room, ни элемента списка. - Два подряд события без `message_id` не приводят к дублю ключа в `LazyColumn`. - Отброшенные кадры считаются и логируются без утечки секретов. - Все существующие тесты зелёные, ни один не удалён и не ослаблен. ## Required tests Новые тесты — рядом с существующими, в `app/src/test/java/app/hermes/mobile/`. `core/network/StaleSocketIsolationTest.kt` - колбэк `onFailure` от предыдущего сокета после успешного переподключения не переводит состояние в `Failed`; - `pendingRequests` активного соединения не обрываются мёртвым сокетом. `core/network/EventOrderingTest.kt` - 500 последовательных `message.delta` при `extraBufferCapacity`, заведомо меньшем нагрузки, приходят подписчику в исходном порядке — на всех трёх хопах; - склеенный из дельт текст совпадает с исходным побайтово. `core/model/GatewayEventValidationTest.kt` - `"session_id": null` → `sessionId == null`, не `"null"`; - поле-объект вместо строки не бросает и не роняет разбор кадра; - события без `message_id` / `tool_id` / `request_id` отбрасываются; - корректное событие после отброшенного разбирается нормально (парсер не «залипает»). `core/repository/EmptyIdRejectionTest.kt` - событие без `message_id`, пропущенное через `UnifiedSessionRepository`, не создаёт сообщения ни через `MessageStart`, ни через `MessageDelta`, ни через `MessageComplete`. Каждый из этих тестов обязан падать на `ba5f0466f3fcb83fc2367ca61727ddb897529f88` и проходить после fix'а. Оба состояния проверяются и указываются в отчёте обоими кодерами независимо. Для пункта 1 юнит-тест невозможен. Проверка — на устройстве или эмуляторе: подключение к реальному `hermes serve --host 0.0.0.0` по http, с приложением логом `adb logcat`. Если устройство недоступно, пункт помечается `UNVERIFIED` с указанием причины; писать «работает» на основании чтения кода запрещено. ## Required verification ```text ./gradlew --no-daemon testDebugUnitTest ./gradlew --no-daemon lint ./gradlew --no-daemon assembleDebug ``` Требуется JDK 17 и Android SDK с `compileSdk 35`. Если среда не позволяет выполнить команду — указать это явно вместе с текстом ошибки. Формулировки `green`, `PASS`, `готово`, `закрыто` без фактического запуска не допускаются (`AGENTS.md §3`). Отдельно приложить вывод `git diff --stat` между base SHA и результатом. ## Result `agents/antigravity/done/TASK-2026-08-24-01-hermes-transport-and-lan-reachability.md` Один файл, три раздела с явными заголовками: `## Кодер 1`, `## Кодер 2 (review + доработка)`, `## Вердикт оркестратора`. Обязательное содержимое каждого — по `AGENTS.md §4`: base SHA, resulting SHA, изменённые файлы, фактически выполненные команды и их вывод, findings по шкале, список `UNVERIFIED`, оставшиеся blockers и tech debt. CRITICAL не считается закрытым по заявлению исполнителя: нужен независимый re-review с указанием fix SHA. ## Очередь следующих заданий Не выполнять в рамках этого задания. Порядок определён зависимостями, полный разбор — в отчёте аудита. | № | Тема | Ключевые находки | |---|---|---| | 02 | Целостность хранения и таймлайна | DATA-01, 02, 03, 04, 06, 08 · BUILD-04 | | 03 | Критичный UX | UI-01, 02, 03, 04, 06 · DATA-05 | | 04 | CI и проверяемость тестов | BUILD-03, 05 · TEST-01, 02, 03, 04 | | 05 | Гигиена репозитория | BUILD-01, 06, 07, 08 | | 06 | Авторизация, TLS, секреты | SEC-02, 03, 04, 05, 06, 07, 08 · NET-08 | | 07 | Протокол сопряжения и hermes-pair | PAIR-01…07 · SEC-09, 10 | | 08 | Жизненный цикл соединения и фон | NET-05, 06, 07, 09 · DATA-11, 12 · UI-11 | | 09 | Производительность и атрибуция | DATA-07, 09, 10 · UI-07 | | 10 | Чистка мёртвого слоя и релиз | DEAD-01, 02 · BUILD-02 · UI-05, 08, 09, 10 |