# 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 1. `exportSchema = true` включён, но `schemas/1.json` не закоммичен — миграции по-прежнему не от чего писать. 2. `fallbackToDestructiveMigration()` убран, а миграции с 1 на 2 нет вообще: приложение теперь падает при апгрейде вместо тихой потери данных. Проверить, что тест миграции существует и проходит. 3. Порядок починен только в одном из путей чтения. Проверить оба: `getSessionWithDetails` и поток `getSessionsFlow` → `sessions`. 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.kt` — `updatedAt` бампится при вставке сообщения. Каждый тест обязан падать на base SHA и проходить после fix'а. Оба состояния проверяются обоими кодерами независимо и указываются в отчёте. ## Required verification ```text ./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.