14 KiB
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,33 — exportSchema = false вместе с fallbackToDestructiveMigration(). При несовпадении версии Room без предупреждения удаляет базу целиком: все сессии, сообщения, хосты, привязки. Схема не экспортируется, поэтому написать миграцию потом будет не от чего.
2. Порядок сообщений не определён (DATA-02). Entities.kt:80-91 — UnifiedSessionWithDetails собирается через @Relation, который не поддерживает ORDER BY. Список messages приходит в порядке, который вернёт SQLite. На этом же списке строится контекст синхронизации между хостами (UnifiedContextBuilder), то есть искажается не только отображение. Тесты дефект не ловят: FakeDaos.kt:70-76 возвращает порядок вставки.
3. Неатомарные обновления теряют сообщения (DATA-03). UnifiedSessionRepository.kt:404-408 — flow.value = flow.value + message; :415-430 — toMutableList() с последующим присваиванием. Оба выполняются для событий разных хостов параллельно на Dispatchers.Default. Два одновременных обновления — одно теряется.
4. Запись в БД на каждую дельту, без порядка (DATA-04). UnifiedSessionRepository.kt:410-412,435-445 — на каждый фрагмент стрима запускается независимая корутина, пишущая полный текст сообщения. Порядок их выполнения не гарантирован, поэтому в базе может закрепиться более ранний префикс: после перезапуска сообщение обрезано. Плюс сотни записей в SQLite на один ответ.
5. Гонка при создании нативной сессии (DATA-06). UnifiedSessionRepository.kt:148-198 — ensureAttachedRuntimeSession читает привязку и создаёт сессию на хосте без блокировки. Два быстрых промпта дадут два 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 и по таймеру не чаще одного раза в 1–2 с. Все записи одного сообщения — через единый сериализованный путь, гарантирующий порядок. Обосновать выбранный механизм.
Требование: обрыв процесса в середине стрима не должен оставлять в БД текст длиннее того, что реально пришло.
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
exportSchema = trueвключён, ноschemas/1.jsonне закоммичен — миграции по-прежнему не от чего писать.fallbackToDestructiveMigration()убран, а миграции с 1 на 2 нет вообще: приложение теперь падает при апгрейде вместо тихой потери данных. Проверить, что тест миграции существует и проходит.- Порядок починен только в одном из путей чтения. Проверить оба:
getSessionWithDetailsи потокgetSessionsFlow→sessions. - Тай-брейк по
idне добавлен — тест зелёный на разныхcreatedAtи мигает на одинаковых. Требование: тест обязан содержать два сообщения с идентичнымcreatedAt. FakeUnifiedSessionDaoоставлен возвращающим порядок вставки — тест наDATA-02не может упасть на base SHA. Не падает — тест не годится.update { }расставлен не везде: остались присваиванияflow.value = flow.value.…. Проверить поиском по файлу, а не выборочно.- Батчинг сделан через
debounceна потоке, который отменяется вместе с scope репозитория — последний фрагмент теряется при закрытии экрана. Проверить сценарий «стрим завершился, приложение сразу свёрнуто». Mutexвзят на один общий объект вместо пары ключей — параллельные сессии сериализуются между собой, отзывчивость падает. Это регресс.- В отчёте
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.kt — updatedAt бампится при вставке сообщения.
Каждый тест обязан падать на 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.