hermes-android/agy-work/TASK-2026-08-24-02-persistence-integrity.md

14 KiB
Raw Blame History

Task 02: Целостность хранения и таймлайна (hermes-android)

Repo: ochenstarik-ui/hermes-android Assigned to: Antigravity (режим оркестратора, два кодера) Priority: CRITICAL (потеря и порча пользовательских данных) Date: 2026-08-24 Base SHA: результат задания 01 — указать фактический SHA при выдаче Зависимость: задание 01 принято. Без исправленного порядка событий (NET-02) тесты на таймлайн будут мигать.

Роли и протокол

Роль Модель Что делает
Оркестратор Antigravity Разбивает работу, маршрутизирует, принимает результат
Кодер 1 Gemini Flash 3.7 high Реализация §Scope целиком
Кодер 2 Gemini Pro high Независимая проверка работы кодера 1 и доработка

Раунд 1 (кодер 1). Сначала тесты из §Required tests, фиксация их падения на base SHA дословным выводом. Затем код по §Scope по пунктам. Где выбор в задании не сделан — вопрос в OPEN QUESTIONS, не молчаливое решение. Раунд 2 (кодер 2). Независимо воспроизводит падение своим запуском. Проходит §Anti-checklist, отмечая каждый пункт проверено — чисто / нарушено — <что>. Доводит минимальным дифом поверх. Findings по шкале CRITICAL/HIGH/MEDIUM/LOW; findings: none допустимо только со списком того, что проверялось. Удалять и ослаблять тесты кодера 1 запрещено. Раунд 3 (оркестратор). Принимает только при фактическом выводе команд, подтверждённом обоими кодерами падении на base SHA, совпадении списка файлов с дифом и отсутствии удалённых тестов.

Проблема

1. Первое изменение схемы стирает всё (DATA-01). HermesDatabase.kt:16,33exportSchema = false вместе с fallbackToDestructiveMigration(). При несовпадении версии Room без предупреждения удаляет базу целиком: все сессии, сообщения, хосты, привязки. Схема не экспортируется, поэтому написать миграцию потом будет не от чего.

2. Порядок сообщений не определён (DATA-02). Entities.kt:80-91UnifiedSessionWithDetails собирается через @Relation, который не поддерживает ORDER BY. Список messages приходит в порядке, который вернёт SQLite. На этом же списке строится контекст синхронизации между хостами (UnifiedContextBuilder), то есть искажается не только отображение. Тесты дефект не ловят: FakeDaos.kt:70-76 возвращает порядок вставки.

3. Неатомарные обновления теряют сообщения (DATA-03). UnifiedSessionRepository.kt:404-408flow.value = flow.value + message; :415-430toMutableList() с последующим присваиванием. Оба выполняются для событий разных хостов параллельно на Dispatchers.Default. Два одновременных обновления — одно теряется.

4. Запись в БД на каждую дельту, без порядка (DATA-04). UnifiedSessionRepository.kt:410-412,435-445 — на каждый фрагмент стрима запускается независимая корутина, пишущая полный текст сообщения. Порядок их выполнения не гарантирован, поэтому в базе может закрепиться более ранний префикс: после перезапуска сообщение обрезано. Плюс сотни записей в SQLite на один ответ.

5. Гонка при создании нативной сессии (DATA-06). UnifiedSessionRepository.kt:148-198ensureAttachedRuntimeSession читает привязку и создаёт сессию на хосте без блокировки. Два быстрых промпта дадут два session.create; вторая привязка перезапишет первую, часть ответов потеряет адресата.

6. Список сессий сортируется по мёртвому полю (DATA-08). Daos.kt:32 сортирует по updatedAt DESC, но updatedAt меняется только в updateActiveHost. Добавление сообщений его не трогает.

Scope

1. Экспорт схемы и миграции (DATA-01, BUILD-04). Включить exportSchema = true, добавить ksp { arg("room.schemaLocation", "$projectDir/schemas") }, закоммитить schemas/1.json. Убрать fallbackToDestructiveMigration(). Добавить androidTestImplementation("androidx.room:room-testing"). Пункт BUILD-04 перенесён сюда из задания 04 сознательно: экспорт схемы неотделим от отказа от destructive-фолбэка. В задании 04 его не дублировать. Схему таблиц в этом задании не менять — только зафиксировать текущую как версию 1.

2. Детерминированный порядок сообщений (DATA-02). Заменить @Relation для messages на явный запрос с ORDER BY createdAt ASC, id ASC и собирать агрегат в DAO/репозитории. Сортировка по одному createdAt недостаточна: System.currentTimeMillis() даёт одинаковые значения для событий одной миллисекунды — нужен стабильный тай-брейк. Одновременно исправить фейк: FakeUnifiedSessionDao обязан возвращать сообщения в перемешанном порядке, чтобы тест на порядок был осмысленным.

3. Атомарные обновления таймлайна (DATA-03). Перевести все изменения MutableStateFlow в репозитории на update { }. Проверить каждое место, где встречается присваивание flow.value = … от предыдущего значения — их несколько, не только два перечисленных.

4. Батчинг записи стрима (DATA-04). Стрим держать в памяти; в БД писать по message.complete и по таймеру не чаще одного раза в 12 с. Все записи одного сообщения — через единый сериализованный путь, гарантирующий порядок. Обосновать выбранный механизм. Требование: обрыв процесса в середине стрима не должен оставлять в БД текст длиннее того, что реально пришло.

5. Блокировка создания сессии (DATA-06). Mutex на пару (sessionId, hostId), удерживаемый на всё время проверки-создания-восстановления. Карту мьютексов чистить при удалении сессии, иначе получится вторая утечка в довесок к DATA-09.

6. Обновление updatedAt (DATA-08). Бампить при каждой вставке сообщения, в той же транзакции, что и сама вставка.

Do not change

  • Схему таблиц и набор полей — только фиксация текущей как v1.
  • Транспорт и парсер событий — задание 01, принято.
  • ViewModel, экраны, навигацию — задание 03.
  • Кэши в памяти sessionMessagesState / hostExecutingState (DATA-09) — задание 09.
  • Логику UnifiedContextBuilder — задание 06.
  • Мёртвый слой — задание 10.

Anti-checklist

  1. exportSchema = true включён, но schemas/1.json не закоммичен — миграции по-прежнему не от чего писать.
  2. fallbackToDestructiveMigration() убран, а миграции с 1 на 2 нет вообще: приложение теперь падает при апгрейде вместо тихой потери данных. Проверить, что тест миграции существует и проходит.
  3. Порядок починен только в одном из путей чтения. Проверить оба: getSessionWithDetails и поток getSessionsFlowsessions.
  4. Тай-брейк по id не добавлен — тест зелёный на разных createdAt и мигает на одинаковых. Требование: тест обязан содержать два сообщения с идентичным createdAt.
  5. FakeUnifiedSessionDao оставлен возвращающим порядок вставки — тест на DATA-02 не может упасть на base SHA. Не падает — тест не годится.
  6. update { } расставлен не везде: остались присваивания flow.value = flow.value.…. Проверить поиском по файлу, а не выборочно.
  7. Батчинг сделан через debounce на потоке, который отменяется вместе с scope репозитория — последний фрагмент теряется при закрытии экрана. Проверить сценарий «стрим завершился, приложение сразу свёрнуто».
  8. Mutex взят на один общий объект вместо пары ключей — параллельные сессии сериализуются между собой, отзывчивость падает. Это регресс.
  9. В отчёте green для команды, которая не запускалась (AGENTS.md §3).

Definition of Done

  • Апгрейд схемы с версии 1 на 2 сохраняет данные — подтверждено тестом миграции, а не рассуждением.
  • Два сообщения с одинаковым createdAt всегда возвращаются в одном и том же порядке.
  • 200 параллельных вставок из двух источников дают ровно 200 сообщений в таймлайне.
  • После стрима из 500 фрагментов содержимое в БД совпадает с содержимым в памяти побайтово.
  • Два одновременных sendPrompt для одной сессии дают ровно один вызов session.create.
  • Список сессий переупорядочивается при получении нового сообщения.
  • Все существующие тесты зелёные, ни один не удалён и не ослаблен.

Required tests

core/storage/MigrationTest.kt (androidTest) — миграция 1→2 на реальной БД с данными, данные на месте. core/storage/MessageOrderingTest.kt — порядок при одинаковом createdAt; тест обязан падать на base SHA. core/repository/ConcurrentTimelineTest.kt — 200 параллельных вставок из двух хостов, ни одно сообщение не потеряно. core/repository/StreamPersistenceTest.kt — 500 дельт, финальное содержимое в БД равно содержимому в памяти; отдельный кейс на обрыв в середине. core/repository/SessionCreateRaceTest.kt — два одновременных sendPrompt, ровно один session.create; тест обязан падать на base SHA. core/repository/SessionOrderingTest.ktupdatedAt бампится при вставке сообщения.

Каждый тест обязан падать на base SHA и проходить после fix'а. Оба состояния проверяются обоими кодерами независимо и указываются в отчёте.

Required verification

./gradlew --no-daemon testDebugUnitTest
./gradlew --no-daemon connectedDebugAndroidTest
./gradlew --no-daemon lint
./gradlew --no-daemon assembleDebug

connectedDebugAndroidTest требует устройства или эмулятора. Если его нет — тест миграции помечается UNVERIFIED с указанием причины, и задание не считается закрытым: DATA-01 без подтверждённой миграции остаётся открытым.

Приложить git diff --stat между base SHA и результатом.

Result

agents/antigravity/done/TASK-2026-08-24-02-persistence-integrity.md — разделы ## Кодер 1, ## Кодер 2 (review + доработка), ## Вердикт оркестратора. Содержимое по AGENTS.md §4.

CRITICAL не закрывается по заявлению исполнителя: нужен независимый re-review с fix SHA.