101 lines
12 KiB
Markdown
101 lines
12 KiB
Markdown
# Task 09: Производительность и правильность атрибуции (hermes-android)
|
||
|
||
**Repo:** `ochenstarik-ui/hermes-android`
|
||
**Assigned to:** Antigravity (режим оркестратора, два кодера)
|
||
**Priority:** MEDIUM (растущая утечка, просадка списка, неверная привязка инструментов)
|
||
**Date:** 2026-08-24
|
||
**Base SHA:** результат задания 08 — указать фактический SHA при выдаче
|
||
**Зависимость:** задания 02 и 03 приняты — атомарные обновления таймлайна и настоящие ViewModel уже на месте.
|
||
|
||
## Роли и протокол
|
||
|
||
| Роль | Модель | Что делает |
|
||
|---|---|---|
|
||
| Оркестратор | 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 по шкале.
|
||
**Раунд 3 (оркестратор).** Приёмка при наличии парных замеров «до/после» от обоих кодеров, а не утверждений «стало быстрее».
|
||
|
||
## Проблема
|
||
|
||
**1. Список сессий делает N+1 запросов (`DATA-07`).** `UnifiedSessionRepository.kt:32-39` — в `map` по списку сессий для каждой вызывается `getSessionWithDetails(entity.id)`, то есть полная выборка со всеми сообщениями и привязками. И так на каждую эмиссию потока, а поток дёргается при любой вставке сообщения. На сотне сессий с историей это заметная просадка главного экрана и лишняя работа во время стрима.
|
||
|
||
**2. Кэши в памяти растут неограниченно (`DATA-09`).** `UnifiedSessionRepository.kt:49-54` — `sessionMessagesState`, `hostExecutingState`, `sessionExecutingState` заполняются через `computeIfAbsent` и очищаются только при удалении сессии. Каждая открытая за сессию работы приложения переписка остаётся в памяти вместе с полной историей. К этому же классу относится карта мьютексов, введённая заданием 02 (`DATA-06`), если её чистка не была сделана.
|
||
|
||
**3. Инструменты и «мышление» цепляются не к тому сообщению (`DATA-10`).** `UnifiedSessionRepository.kt:713-731` — `attachToolToSessionMessage` ищет `lastOrNull { role == ASSISTANT && hostId == host }`. Обработчики `thinking.delta` и `reasoning.delta` (`:585,:597`) используют условие `(it.id == event.messageId || it.role == ASSISTANT)`: из-за `||` дельта уедет в произвольное последнее сообщение ассистента, даже когда `message_id` известен и не совпадает. При двух хостах, пишущих в один таймлайн, карточки инструментов и трассы рассуждений оказываются под чужими сообщениями — то есть ломается ровно то, что README называет host attribution.
|
||
|
||
**4. Автопрокрутка перезапускается на каждый символ (`UI-07`).** `ChatScreen.kt:90-95` — ключ эффекта включает `messages.lastOrNull()?.content?.length`, во время стрима эффект перезапускается десятки раз в секунду, каждый раз отменяя предыдущую анимацию. Индекс `messages.size + approvals.size` на единицу больше последнего допустимого.
|
||
|
||
## Scope
|
||
|
||
**1. Лёгкий список сессий (`DATA-07`).**
|
||
Отдавать в список проекцию: идентификатор, заголовок, активный хост, отметка времени, превью последнего сообщения, счётчик. Одним запросом, без выборки истории. Полные детали грузить только на экране чата.
|
||
Проверить, что при этом не сломался порядок из задания 02 (`updatedAt`) и что поток не переэмитит на каждый фрагмент стрима.
|
||
|
||
**2. Ограничение кэшей (`DATA-09`).**
|
||
Освобождать состояние сессии, когда на него никто не подписан: `WhileSubscribed` с таймаутом либо явная очистка при уходе с экрана чата. Карты `hostExecutingState`, `sessionExecutingState` и мьютексов чистить тем же путём.
|
||
Требование: после открытия и закрытия 50 сессий подряд объём удерживаемых сообщений возвращается к базовому.
|
||
|
||
**3. Точная атрибуция (`DATA-10`).**
|
||
Вести карту `toolId → messageId` и использовать её вместо поиска «последнего сообщения хоста». Условие с `||` в обработчиках `thinking`/`reasoning` заменить на строгое соответствие `messageId`; фолбэк на последнее сообщение — только когда идентификатора нет вовсе, и с явной пометкой в коде, почему это допустимо.
|
||
Проверить сценарий двух хостов, стримящих одновременно в одну сессию.
|
||
|
||
**4. Прокрутка (`UI-07`).**
|
||
Скроллить через `snapshotFlow` с `conflate()`; целевой индекс — последний допустимый; во время активного стрима не анимировать, а держать позицию. Учесть, что пользователь мог прокрутить вверх намеренно: автопрокрутка не должна возвращать его вниз, пока он читает историю.
|
||
|
||
## Do not change
|
||
|
||
- Схему БД сверх добавления нужных проекции индексов; изменение схемы версионируется по правилам задания 02.
|
||
- Транспорт и порядок событий — задания 01 и 08.
|
||
- Логику подтверждений — задание 03.
|
||
- Оформление экранов — задание 10.
|
||
|
||
## Anti-checklist
|
||
|
||
1. Проекция добавлена, но `sessions` по-прежнему вызывает `getSessionWithDetails` в другой ветке. Проверить поиском все вызовы.
|
||
2. Превью последнего сообщения считается подзапросом на строку — N+1 вернулся в другой форме. Проверить фактическое число запросов, а не форму кода.
|
||
3. `WhileSubscribed` применён к потоку, но сама карта `sessionMessagesState` продолжает держать `MutableStateFlow` с историей — освобождается подписка, а не память.
|
||
4. Карта мьютексов из задания 02 не почищена — утечка осталась, просто в другом месте.
|
||
5. Карта `toolId → messageId` не чистится при завершении сообщения — третья утечка.
|
||
6. Строгое соответствие `messageId` введено, но событие с пустым id теперь молча теряет инструмент. Проверить взаимодействие с валидацией задания 01: событие без id туда вообще не должно доходить.
|
||
7. Автопрокрутка «починена» удалением — список перестал следовать за стримом. Это регресс, а не фикс.
|
||
8. Замеры «после» сняты на другом наборе данных, чем «до». Требование: один и тот же сценарий и объём.
|
||
9. В отчёте `green` для незапущенной команды (`AGENTS.md §3`).
|
||
|
||
## Definition of Done
|
||
|
||
- Открытие списка из 100 сессий с историей по 200 сообщений выполняет фиксированное число запросов, не зависящее от числа сессий, — подтверждено счётчиком запросов, а не ощущением.
|
||
- Во время стрима список сессий не перевыбирает историю.
|
||
- После открытия и закрытия 50 сессий удерживаемый объём возвращается к базовому.
|
||
- Инструмент, начатый на хосте A, отображается под сообщением хоста A даже когда хост B стримит одновременно.
|
||
- Трасса рассуждений с известным `message_id` не попадает в чужое сообщение.
|
||
- Прокрутка следует за стримом плавно и не перебивает намеренную прокрутку пользователя вверх.
|
||
- Все существующие тесты зелёные, ни один не удалён и не ослаблен.
|
||
|
||
## Required tests
|
||
|
||
`core/repository/SessionListQueryCountTest.kt` — число запросов при 100 сессиях фиксировано. Обязан падать на base SHA.
|
||
`core/repository/CacheEvictionTest.kt` — после отписки состояние сессии освобождается; карты не растут.
|
||
`core/repository/ToolAttributionTest.kt` — два хоста стримят одновременно, инструменты и рассуждения под своими сообщениями. Обязан падать на base SHA.
|
||
`feature/chat/AutoScrollTest.kt` (androidTest) — прокрутка следует за стримом; ручная прокрутка вверх не перебивается.
|
||
|
||
Замеры, обязательные в отчёте обоих кодеров, на одном сценарии до и после:
|
||
- число SQL-запросов при открытии списка;
|
||
- удерживаемая память после цикла из 50 сессий;
|
||
- частота перезапуска эффекта прокрутки за 10 секунд стрима.
|
||
|
||
## Required verification
|
||
|
||
```text
|
||
./gradlew --no-daemon testDebugUnitTest
|
||
./gradlew --no-daemon connectedDebugAndroidTest
|
||
./gradlew --no-daemon lint
|
||
./gradlew --no-daemon assembleDebug
|
||
```
|
||
|
||
## Result
|
||
|
||
`agents/antigravity/done/TASK-2026-08-24-09-performance-and-attribution.md` — разделы `## Кодер 1`, `## Кодер 2 (review + доработка)`, `## Замеры до/после`, `## Вердикт оркестратора`.
|