# 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.
