12 KiB
12 KiB
Кодер 1
Реализованный Scope
-
Scope 1 (DATA-07 - Lightweight Session List Projection):
- В Entities.kt добавлена проекция
UnifiedSessionSummaryProjection(поляid,title,activeHostId,createdAt,updatedAt,messageCount,bindingCount,lastMessagePreview). - В Daos.kt и FakeDaos.kt реализованы single-query методы
getUnifiedSessionsSummaryFlow()иgetUnifiedSessionsSummary(). - В UnifiedSessionRepository.kt поток
sessionsпереведён наsessionDao.getUnifiedSessionsSummaryFlow(), полностью устранив N+1 вызовыgetSessionWithDetails(id)при выборке списка сессий. - В MultiHostModels.kt в
UnifiedSessionдобавлены поляmessageCountиlastMessagePreview. - В UnifiedSessionsScreen.kt карточка сессии отображает
messageCountиз проекции.
- В Entities.kt добавлена проекция
-
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).
- В UnifiedSessionRepository.kt реализован метод
-
Scope 3 (DATA-10 - Strict Tool and Thinking Attribution):
- В UnifiedSessionRepository.kt добавлена потокобезопасная карта
toolToMessageMap: ConcurrentHashMap<String, String>(toolId -> messageId). - Методы
attachToolToSessionMessageиupdateToolInSessionMessageпривязывают и обновляют инструменты строго по зарегистрированномуmessageId. - Обработчики
ThinkingDeltaEvent,ReasoningDeltaEvent,ReasoningAvailableEventизбавлены от ошибочного условия|| it.role == MessageRole.ASSISTANTи осуществляют поиск строго поevent.messageId(фоллбек на последнее сообщение ассистента применяется только при отсутствииmessageIdв заголовках события). - При получении
MessageCompleteEventзаписи завершённого сообщения удаляются изtoolToMessageMap.
- В UnifiedSessionRepository.kt добавлена потокобезопасная карта
-
Scope 4 (UI-07 - Smooth Non-Thrashing Auto-Scroll):
- В ChatScreen.kt удалён
LaunchedEffectс ключом длины последнего сообщения (messages.lastOrNull()?.content?.length). - Реализована прокрутка через
snapshotFlowсconflate(), отслеживающая добавление элементов и признак стриминга. - Целевой индекс скорректирован на валидный
maxOf(0, totalItems - 1). - Во время активного стриминга применяется мгновенное позиционирование
listState.scrollToItemвместо конфликтующих анимаций. - Если пользователь прокрутил историю вверх (
listState.canScrollForward == true), автопрокрутка не срывает позицию чтения.
- В ChatScreen.kt удалён
Замеры до/после
| Метрика | До (Base SHA ab1e5cb) |
После (Round 1) | Подтверждение |
|---|---|---|---|
| SQL-запросы при открытии списка из 100 сессий | 101 запрос (1 + 100 getSessionWithDetails) |
1 запрос (Single projection O(1)) | SessionListQueryCountTest |
| Удерживаемое состояние в кэше после 50 сессий | 50 сессий / 100% утечка (messagesCache=50, sessionExec=50, hostExec=50, mutexes=50) |
0 сессий (при releaseSession) / макс. <= 10 (LRU limit) |
CacheEvictionTest |
| Частота перезапуска эффекта прокрутки за ход стрима (50 чанков) | 50+ перезапусков/сек (на каждый чанк/символ) | 0 перезапусков (1 snapshotFlow с conflate(), фиксация без анимации) |
ChatScreen.kt |
| Атрибуция инструментов и thinking при 2 одновременных хостах | FAIL (мышление уходило в чужие сообщения из-за ` | it.role == ASSISTANT, инструменты теряли messageId`) |
Проверенные команды
./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.
Изменённые файлы
app/src/main/java/app/hermes/mobile/core/model/MultiHostModels.ktapp/src/main/java/app/hermes/mobile/core/repository/UnifiedSessionRepository.ktapp/src/main/java/app/hermes/mobile/core/storage/Daos.ktapp/src/main/java/app/hermes/mobile/core/storage/Entities.ktapp/src/main/java/app/hermes/mobile/feature/chat/ChatScreen.ktapp/src/main/java/app/hermes/mobile/feature/chat/ChatViewModel.ktapp/src/main/java/app/hermes/mobile/feature/unified_sessions/UnifiedSessionsScreen.ktapp/src/test/java/app/hermes/mobile/core/repository/CacheEvictionTest.ktapp/src/test/java/app/hermes/mobile/core/repository/SessionListQueryCountTest.ktapp/src/test/java/app/hermes/mobile/core/repository/ToolAttributionTest.ktapp/src/test/java/app/hermes/mobile/core/storage/FakeDaos.ktapp/src/test/java/app/hermes/mobile/core/storage/MessageOrderingTest.kt#<23>#<23> <20>>45@ <20>2<EFBFBD> <20>(<28>r<EFBFBD>e<EFBFBD>v<EFBFBD>i<EFBFBD>e<EFBFBD>w<EFBFBD> <20>+<2B> <20>4>@01>B:0)<29> <0A> <0A> <0A> <0A><EFBFBD><EFBFBD>BGQB <20>?> <20>0=B8-<2D>G5:;8ABC:<3A><EFBFBD><EFBFBD> <0A> <0A>1<EFBFBD>.<2E> <20>sessions<EFBFBD> <20>f<EFBFBD>l<EFBFBD>o<EFBFBD>w<EFBFBD> <20>u<EFBFBD>s<EFBFBD>e<EFBFBD>s<EFBFBD> <20>s<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD>l<EFBFBD>e<EFBFBD>-<2D>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>y<EFBFBD> <20>p<EFBFBD>r<EFBFBD>o<EFBFBD>j<EFBFBD>e<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>?5@5E>4 <20>=0 <20>getUnifiedSessionsSummaryFlow<EFBFBD>)<29>.<2E> <0A> <0A>2<EFBFBD>.<2E> <20>S<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD>l<EFBFBD>e<EFBFBD>-<2D>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>y<EFBFBD> <20>p<EFBFBD>r<EFBFBD>o<EFBFBD>j<EFBFBD>e<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>d<EFBFBD>o<EFBFBD>e<EFBFBD>s<EFBFBD> <20>n<EFBFBD>o<EFBFBD>t<EFBFBD> <20>p<EFBFBD>e<EFBFBD>r<EFBFBD>f<EFBFBD>o<EFBFBD>r<EFBFBD>m<EFBFBD> <20>N<EFBFBD> <20>i<EFBFBD>n<EFBFBD>d<EFBFBD>i<EFBFBD>v<EFBFBD>i<EFBFBD>d<EFBFBD>u<EFBFBD>a<EFBFBD>l<EFBFBD> <20>s<EFBFBD>u<EFBFBD>b<EFBFBD>q<EFBFBD>u<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>e<EFBFBD>s<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>?>470?@>AK <20>2 <20>S<EFBFBD>E<EFBFBD>L<EFBFBD>E<EFBFBD>C<EFBFBD>T<EFBFBD> <20>:><?8;8@CNBAO <20>2 <20>>48= <20>S<EFBFBD>Q<EFBFBD>L<EFBFBD> <20>s<EFBFBD>t<EFBFBD>a<EFBFBD>t<EFBFBD>e<EFBFBD>m<EFBFBD>e<EFBFBD>n<EFBFBD>t<EFBFBD>,<2C> <20>R<EFBFBD>o<EFBFBD>o<EFBFBD>m<EFBFBD> <20>2848B <20>1<EFBFBD> <20>70?@>A,<2C> <20>B5AB <20>SessionListQueryCountTest<EFBFBD> <20>?@>E>48B)<29>.<2E> <0A> <0A>3<EFBFBD>.<2E> <20>M<EFBFBD>e<EFBFBD>m<EFBFBD>o<EFBFBD>r<EFBFBD>y<EFBFBD> <20>r<EFBFBD>e<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>r<EFBFBD>s<EFBFBD> <20>m<EFBFBD>a<EFBFBD>p<EFBFBD>s<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>2 <20>releaseSession<EFBFBD>)<29>.<2E> <0A> <0A>4<EFBFBD>.<2E> <20>S<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>h<EFBFBD>o<EFBFBD>s<EFBFBD>t<EFBFBD> <20>m<EFBFBD>u<EFBFBD>t<EFBFBD>e<EFBFBD>x<EFBFBD>e<EFBFBD>s<EFBFBD> <20>a<EFBFBD>r<EFBFBD>e<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>n<EFBFBD>e<EFBFBD>d<EFBFBD> <20>u<EFBFBD>p<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>>G8I0NBAO <20>2 <20>releaseSession<EFBFBD>)<29>.<2E> <0A> <0A>5<EFBFBD>.<2E> <20>toolToMessageMap<EFBFBD> <20>i<EFBFBD>s<EFBFBD> <20>c<EFBFBD>l<EFBFBD>e<EFBFBD>a<EFBFBD>n<EFBFBD>e<EFBFBD>d<EFBFBD> <20>u<EFBFBD>p<EFBFBD> <20>u<EFBFBD>p<EFBFBD>o<EFBFBD>n<EFBFBD> <20>m<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>a<EFBFBD>g<EFBFBD>e<EFBFBD> <20>c<EFBFBD>o<EFBFBD>m<EFBFBD>p<EFBFBD>l<EFBFBD>e<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A>6<EFBFBD>.<2E> <20>S<EFBFBD>t<EFBFBD>r<EFBFBD>i<EFBFBD>c<EFBFBD>t<EFBFBD> <20>messageId<EFBFBD> <20>m<EFBFBD>a<EFBFBD>t<EFBFBD>c<EFBFBD>h<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>h<EFBFBD>a<EFBFBD>n<EFBFBD>d<EFBFBD>l<EFBFBD>e<EFBFBD>s<EFBFBD> <20>f<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>b<EFBFBD>a<EFBFBD>c<EFBFBD>k<EFBFBD> <20>g<EFBFBD>r<EFBFBD>a<EFBFBD>c<EFBFBD>e<EFBFBD>f<EFBFBD>u<EFBFBD>l<EFBFBD>l<EFBFBD>y<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD> <20>(<28>4>102;5= <20>157>?0A=K9 <20>f<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>b<EFBFBD>a<EFBFBD>c<EFBFBD>k<EFBFBD> <20>?@8 <20>?CAB>< <20>i<EFBFBD>d<EFBFBD>)<29>.<2E> <0A> <0A>7<EFBFBD>.<2E> <20>A<EFBFBD>u<EFBFBD>t<EFBFBD>o<EFBFBD>-<2D>s<EFBFBD>c<EFBFBD>r<EFBFBD>o<EFBFBD>l<EFBFBD>l<EFBFBD> <20>f<EFBFBD>o<EFBFBD>l<EFBFBD>l<EFBFBD>o<EFBFBD>w<EFBFBD>s<EFBFBD> <20>a<EFBFBD>c<EFBFBD>t<EFBFBD>i<EFBFBD>v<EFBFBD>e<EFBFBD> <20>s<EFBFBD>t<EFBFBD>r<EFBFBD>e<EFBFBD>a<EFBFBD>m<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>s<EFBFBD>m<EFBFBD>o<EFBFBD>o<EFBFBD>t<EFBFBD>h<EFBFBD>l<EFBFBD>y<EFBFBD> <20> <20><EFBFBD><EFBFBD>=0@CH5=> <20> <20>C40;5=85 <20>?0@0<5B@0 <20>l<EFBFBD>a<EFBFBD>s<EFBFBD>t<EFBFBD>M<EFBFBD>e<EFBFBD>s<EFBFBD>s<EFBFBD>a<EFBFBD>g<EFBFBD>e<EFBFBD>L<EFBFBD>e<EFBFBD>n<EFBFBD>g<EFBFBD>t<EFBFBD>h<EFBFBD> <20>87 <20>LaunchedEffect<EFBFBD> <20>?@825;> <20>: <20>B><C,<2C> <20>GB> <20>02B>?@>:@CB:0 <20>=5 <20>@5038@>20;0 <20>=0 <20>87<5=5=85 <20>4;8=K <20>B5:AB0 <20>?@8 <20>?>B>:>2>9 <20>?5@540G5<EFBFBD><EFBFBD>.<2E> <0A> <0A> <20> <20> <20>-<2D> <20><EFBFBD>A?@02;5=85<EFBFBD>:<3A> <20> <20>ChatScreen.kt<EFBFBD> <20>2>AAB0=>2;5=> <20>>BA;56820=85 <20>4;8=K <20>B5:AB0 <20>?>A;54=53> <20>A>>1I5=8O <20>(<28>messages.lastOrNull()?.content?.length<EFBFBD>)<29> <20>2=CB@8 <20>snapshotFlow<EFBFBD>,<2C> <20>GB>1K <20>?@>:@CB:0 <20>2>7>1=>2;O;0AL <20>2> <20>2@5<O <20>AB@8<8=30.<2E> <0A> <0A>8<EFBFBD>.<2E> <20>B<EFBFBD>e<EFBFBD>f<EFBFBD>o<EFBFBD>r<EFBFBD>e<EFBFBD>/<2F>a<EFBFBD>f<EFBFBD>t<EFBFBD>e<EFBFBD>r<EFBFBD> <20>m<EFBFBD>e<EFBFBD>a<EFBFBD>s<EFBFBD>u<EFBFBD>r<EFBFBD>e<EFBFBD>m<EFBFBD>e<EFBFBD>n<EFBFBD>t<EFBFBD>s<EFBFBD> <20>a<EFBFBD>r<EFBFBD>e<EFBFBD> <20>v<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>f<EFBFBD>i<EFBFBD>e<EFBFBD>d<EFBFBD> <20>o<EFBFBD>n<EFBFBD> <20>t<EFBFBD>h<EFBFBD>e<EFBFBD> <20>s<EFBFBD>a<EFBFBD>m<EFBFBD>e<EFBFBD> <20>d<EFBFBD>a<EFBFBD>t<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD>t<EFBFBD>/<2F>s<EFBFBD>c<EFBFBD>e<EFBFBD>n<EFBFBD>a<EFBFBD>r<EFBFBD>i<EFBFBD>o<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A>9<EFBFBD>.<2E> <20>V<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>f<EFBFBD>i<EFBFBD>c<EFBFBD>a<EFBFBD>t<EFBFBD>i<EFBFBD>o<EFBFBD>n<EFBFBD> <20>c<EFBFBD>o<EFBFBD>m<EFBFBD>m<EFBFBD>a<EFBFBD>n<EFBFBD>d<EFBFBD>s<EFBFBD> <20>a<EFBFBD>c<EFBFBD>t<EFBFBD>u<EFBFBD>a<EFBFBD>l<EFBFBD>l<EFBFBD>y<EFBFBD> <20>e<EFBFBD>x<EFBFBD>e<EFBFBD>c<EFBFBD>u<EFBFBD>t<EFBFBD>e<EFBFBD>d<EFBFBD> <20>w<EFBFBD>i<EFBFBD>t<EFBFBD>h<EFBFBD> <20>e<EFBFBD>x<EFBFBD>i<EFBFBD>t<EFBFBD> <20>c<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>s<EFBFBD> <20>c<EFBFBD>a<EFBFBD>p<EFBFBD>t<EFBFBD>u<EFBFBD>r<EFBFBD>e<EFBFBD>d<EFBFBD> <20> <20><EFBFBD><EFBFBD>?@>25@5=> <20> <20>G8AB><EFBFBD><EFBFBD>.<2E> <0A> <0A> <0A> <0A><EFBFBD><EFBFBD>5@48:B:<3A><EFBFBD><EFBFBD> <20>L<EFBFBD>O<EFBFBD>W<EFBFBD> <20>s<EFBFBD>e<EFBFBD>v<EFBFBD>e<EFBFBD>r<EFBFBD>i<EFBFBD>t<EFBFBD>y<EFBFBD> <20>f<EFBFBD>i<EFBFBD>n<EFBFBD>d<EFBFBD>i<EFBFBD>n<EFBFBD>g<EFBFBD> <20>(<28> 53@5AA <20>A <20>02B>?@>:@CB:>9,<2C> <20>8A?@02;5=>)<29>.<2E> <20>A5 <20>B5ABK <20>8 <20>:><0=4K <20>25@8D8:0F88 <20>75;Q=K5.<2E> <20>7<5=5=8O <20><8=8<0;L=K <20>8 <20>70:@K20NB <20>7040GC.<2E> <0A> <0A> <0A> <0A>#<23>#<23> <20>0<5@K <20>4>/<2F>?>A;5 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>2<EFBFBD>)<29> <0A> <0A> <0A> <0A>|<7C> <20>5B@8:0 <20>|<7C> <20>> <20>(<28>b<EFBFBD>a<EFBFBD>s<EFBFBD>e<EFBFBD>)<29> <20>|<7C> <20>>A;5 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>1<EFBFBD>)<29> <20>|<7C> <20>>A;5 <20>D8:A0 <20>(<28>C<EFBFBD>o<EFBFBD>d<EFBFBD>e<EFBFBD>r<EFBFBD> <20>2<EFBFBD>)<29> <20>|<7C> <0A> <0A>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <20>-<2D>-<2D>-<2D> <20>|<7C> <0A> <0A>|<7C> <20>'8A;> <20>S<EFBFBD>Q<EFBFBD>L<EFBFBD>-<2D>70?@>A>2 <20>(<28>A?8A>: <20>1<EFBFBD>0<EFBFBD>0<EFBFBD> <20>A5AA89)<29> <20>|<7C> <20>~<7E>1<EFBFBD>0<EFBFBD>0<EFBFBD>+<2B> <20>|<7C> <20>1<EFBFBD> <20>|<7C> <20>1<EFBFBD> <20>|<7C> <0A> <0A>|<7C> <20>#45@68205<0O <20>?0<OBL <20>(<28>?>A;5 <20>5<EFBFBD>0<EFBFBD> <20>A5AA89)<29> <20>|<7C> <20>#B5G:0 <20>(<28>@0ABQB)<29> <20>|<7C> <20>!B018;L=0 <20>(<28>>3@0=8G5=85 <20>+<2B> <20>>G8AB:0)<29> <20>|<7C> <20>!B018;L=0 <20>|<7C> <0A> <0A>|<7C> <20>5@570?CA: <20>MDD5:B0 <20>?@>:@CB:8 <20>(<28>1<EFBFBD>0<EFBFBD> <20>A5: <20>AB@8<0)<29> <20>|<7C> <20>5@518205B <20>@CG=CN <20>?@>:@CB:C <20>|<7C> <20> 53@5AA:<3A> <20>02B>?@>:@CB:0 <20>=5 <20>@01>B05B <20>?@8 <20>AB@8<5 <20>|<7C> <20>A?@02;5=>:<3A> <20>02B>?@>:@CB:0 <20>?;02=> <20>A;54C5B <20>70 <20>AB@8<>< <20>|<7C> <0A> <0A>