21 KiB
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
- Сначала пишет тесты из §Required tests и фиксирует их падение на base SHA. Вывод падения — в отчёт дословно.
- Только после этого правит код по §Scope, пункт за пунктом.
- Архитектурных решений не принимает. Где в задании выбор не сделан явно — останавливается и записывает вопрос в раздел
OPEN QUESTIONSсвоего отчёта, а не выбирает молча. - В отчёте: base SHA, список изменённых файлов, диф по каждому пункту §Scope, фактический вывод команд §Required verification.
Раунд 2 — кодер 2
Работу кодера 1 не переписывает целиком. Порядок строго такой:
- Независимо воспроизводит падение тестов на base SHA — своим запуском, не по отчёту кодера 1.
- Читает диф против §Definition of Done и §Anti-checklist. Каждый пункт анти-чеклиста отмечает явно:
проверено — чисто/нарушено — <что именно>. - Найденное исправляет сам, минимальным дифом поверх работы кодера 1.
- Пишет 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 и подключить его в <application>. 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 обязан пройти каждый пункт и отметить результат явно. Это перечень способов сдать задание формально невыполненным при зелёных тестах.
- Проверка
webSocket !== activeWebSocketдобавлена, но поле осталось обычнымvar— гонка сохранилась, тест зелёный случайно. tryEmit-фолбэк убран вJsonRpcGatewayClient, но остался вHermesHostRuntimeи/илиHermesConnectionManager. Проверить все три файла поимённо.- Порядок событий «починен» переводом на
emitпрямо вonMessage— поток OkHttp теперь блокируется на переполненном буфере. Это регресс, а не фикс. - Валидация id добавлена в
GatewayEvent.parse, но пустой id всё ещё попадает в Room и в ключLazyColumnпо другому пути. Проверить оба обработчика в репозитории, а не только парсер. - Тест написан на фейке, который не воспроизводит дефект (как
FakeUnifiedSessionDao, скрывающий неопределённый порядок Room). Требование: каждый тест из §Required tests обязан падать наba5f046. Не падает — тест не годится. network_security_config.xmlдобавлен, ноandroid:usesCleartextTraffic="false"в манифесте оставлен без разбора, какой из них применяется. Противоречивая пара — не «сделано».- В отчёте написано
green/PASSдля команды, которая фактически не запускалась из-за отсутствия JDK или Android SDK. Это прямое нарушениеAGENTS.md §3. - Пустой
catchзаменён на логирование, которое печатает тело кадра целиком — то есть в logcat уезжают тикеты и содержимое переписки.
Definition of Done
- Запрос к хосту с
allowCleartext = trueи адресомhttp://<lan-ip>:<port>доходит до сети; запрос к хосту с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
./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 |