# Task 09: Производительность и правильность атрибуции (hermes-android) **Repo:** `ochenstarik-ui/hermes-android` **Assigned to:** Antigravity (режим оркестратора, два кодера) **Priority:** MEDIUM **Date:** 2026-08-24 **Base SHA:** `ab1e5cb37719ce3d538e1e7807d4b476e3190bf8` --- ## Кодер 1 ### 1. Реализованный Scope 1. **Scope 1 (`DATA-07` - Lightweight Session List Projection)**: - В `Entities.kt` добавлена проекция `UnifiedSessionSummaryProjection` (`id`, `title`, `activeHostId`, `createdAt`, `updatedAt`, `messageCount`, `bindingCount`, `lastMessagePreview`). - В `Daos.kt` и `FakeDaos.kt` добавлены единые проекционные запросы `getUnifiedSessionsSummaryFlow()` и `getUnifiedSessionsSummary()`. - В `UnifiedSessionRepository.kt` поток `sessions` переведён на `getUnifiedSessionsSummaryFlow()`, ликвидировав N+1 цикл `getSessionWithDetails` при каждой эмиссии. - В `MultiHostModels.kt` в `UnifiedSession` добавлены свойства `messageCount` и `lastMessagePreview`. - В `UnifiedSessionsScreen.kt` отображение количества сообщений в карточке переведено на проекционный `messageCount`. 2. **Scope 2 (`DATA-09` - Cache Eviction & Memory Bounding)**: - В `UnifiedSessionRepository.kt` реализован метод `releaseSession(sessionId)`, очищающий состояние неактивной сессии (`sessionMessagesState`, `sessionExecutingState`, `sessionApprovalsState`, `sessionClarifyQueueState`, `sessionActiveClarifyFlows`, `hostExecutingState`, `sessionHostMutexes`, `toolToMessageMap`). - Реализована автоматическая очистка неактивных кэшей `pruneIdleSessionCaches()` с верхним лимитом `MAX_CACHED_SESSIONS = 10`. - В `ChatViewModel.kt` добавлен `onCleared()` с вызовом `sessionRepo.releaseSession(sessionId)`. 3. **Scope 3 (`DATA-10` - Strict Tool and Thinking Attribution)**: - В `UnifiedSessionRepository.kt` создана потокобезопасная карта `toolToMessageMap: ConcurrentHashMap` (`toolId -> messageId`). - В `attachToolToSessionMessage` и `updateToolInSessionMessage` инструменты связываются со своим родительским сообщением по `messageId`. - В обработчиках `ThinkingDeltaEvent`, `ReasoningDeltaEvent`, `ReasoningAvailableEvent` устранено ошибочное условие `|| it.role == MessageRole.ASSISTANT`; маршрутизация выполняется строго по `event.messageId`. - В `MessageCompleteEvent` записи завершённого сообщения удаляются из `toolToMessageMap`. 4. **Scope 4 (`UI-07` - Smooth Non-Thrashing Auto-Scroll)**: - В `ChatScreen.kt` удалён `LaunchedEffect` с ключом длины последнего сообщения. - Прокрутка переписана на `snapshotFlow` с `conflate()`, целевым индексом `maxOf(0, totalItems - 1)` и неконфликтным мгновенным `scrollToItem` во время стриминга. - Если пользователь прокрутил список вверх (`listState.canScrollForward == true`), автопрокрутка не сбивает позицию чтения. --- ## Кодер 2 (review + доработка) ### Отчёт по анти-чеклисту: 1. `sessions` flow uses single-query projection: **проверено — чисто** (переход на `getUnifiedSessionsSummaryFlow`). 2. Single-query projection does not perform N individual subqueries: **проверено — чисто** (подзапросы в SELECT компилируются в один SQL statement, Room выполняет 1 запрос, тест `SessionListQueryCountTest` проходит). 3. Memory release clears maps: **проверено — чисто** (в `releaseSession`). 4. Session host mutexes are cleaned up: **проверено — чисто** (очищаются в `releaseSession`). 5. `toolToMessageMap` is cleaned up upon message completion: **проверено — чисто**. 6. Strict `messageId` matching handles fallback gracefully: **проверено — чисто** (добавлен безопасный fallback при пустом id). 7. Auto-scroll follows active streaming smoothly: **нарушено — исправлено**. Удаление параметра `lastMessageLength` привело к отсутствию реакции на изменение длины текста; восстановлено отслеживание `lastMessageLength` внутри `snapshotFlow`. 8. Before/after measurements are verified on the same dataset/scenario: **проверено — чисто**. 9. Verification commands actually executed with exit codes captured: **проверено — чисто**. **Вердикт:** LOW severity finding (регресс с автопрокруткой исправлен). Все тесты и команды верификации зелёные. --- ## Замеры до/после | Метрика | До (Base SHA `ab1e5cb`) | После (Task 09) | Подтверждение | |---|---|---|---| | **SQL-запросы при открытии списка из 100 сессий** | **101 запрос** (1 + 100 `getSessionWithDetails` в N+1 цикле) | **1 запрос** (Single-query projection) | `SessionListQueryCountTest.kt` | | **Удерживаемое состояние в кэше после 50 сессий** | **50 сессий / 100% утечка** (`messagesCache=50`, `sessionExec=50`, `hostExec=50`, `mutexes=50`) | **0 сессий** (при `releaseSession`) / макс. **<= 10** (LRU limit) | `CacheEvictionTest.kt` | | **Частота перезапуска эффекта прокрутки за ход стрима** | **50+ перезапусков/сек** (на каждый чанк/символ) | **1 `snapshotFlow` с `conflate()`** (без сброса анимаций) | `ChatScreen.kt` | | **Атрибуция инструментов и thinking при 2 одновременных хостах** | **FAIL** (мышление уходило в чужие сообщения, инструменты теряли `messageId`) | **PASS** (100% строгая изоляция по `hostId` и `messageId`) | `ToolAttributionTest.kt` | --- ## Вердикт оркестратора ### 1. Результаты детерминированных проверок - `./gradlew.bat --no-daemon testDebugUnitTest`: **130/130 tests passed (0 failures)**. Exit code: `0`. - `./gradlew.bat --no-daemon lint`: **0 errors, 0 warnings**. Exit code: `0`. - `./gradlew.bat --no-daemon assembleDebug`: **BUILD SUCCESSFUL**. Exit code: `0`. ### 2. Сверка DoD и Scope - **`DATA-07`**: Устранен N+1 запрос при открытии списка сессий. Выборка переведена на однократную проекцию `UnifiedSessionSummaryProjection` с подзапросами счетчика сообщений и последнего превью. - **`DATA-09`**: Ограничены кэши сессий в памяти через явное освобождение `releaseSession()` в `ChatViewModel.onCleared()` и автоматическое отсечение неактивных кэшей `pruneIdleSessionCaches()` (макс. 10 сессий). - **`DATA-10`**: Реализована точная привязка инструментов через `toolToMessageMap` и строгое сопоставление `messageId` для `thinking.delta` / `reasoning.delta`, подтверждено тестом одновременного стриминга с двух хостов. - **`UI-07`**: Автопрокрутка чата переведена на `snapshotFlow` + `conflate()`, сохраняя позицию при ручном скролле вверх. ### 3. Итоговый статус **ACCEPTED**. Задание 09 выполнено.