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

119 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` и по таймеру не чаще одного раза в 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` и поток `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.