119 lines
14 KiB
Markdown
119 lines
14 KiB
Markdown
# 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.
|