feat(ai-chat): load full transcript for model history (drop 50-msg window) #202
Reference in New Issue
Block a user
Delete Branch "feat/ai-chat-full-history"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что и зачем
Убирает оконный лимит в 50 сообщений на историю, которую AI-агент получает каждый ход.
Раньше
streamChatвосстанавливал диалог черезfindRecent(chatId, ws, 50)— скользящее окно из 50 последних строк, из-за чего у чатов длиннее ~50 сообщений начало диалога выпадало из контекста модели.Изменение
streamChatтеперь грузит весь неудалённый транскрипт через уже существующийfindAllByChat(хронологически, oldest → newest).findRecent; обновлены комментарии и текст warning-лога (findAllByChatтеперь обслуживает и Markdown-экспорт, и историю модели).Проверки
AiChatMessage[], oldest → newest, те жеbaseFieldsбезtsv).findRecentне осталось; type-check чистый; логика тестов не тронута.🤖 Generated with Claude Code
Code review — PR #202: загрузка полного транскрипта в историю модели (отказ от окна 50 сообщений)
Вердикт: Approve with comments (одобрить с замечаниями).
Изменение корректное и аккуратное: это like-for-like замена
findRecentна уже существующийfindAllByChat. Воркспейс-скоупинг (workspaceId) и фильтр soft-delete сохранены, набор колонок тот же (безtsv), мёртвыйfindRecentудалён без единой висячей ссылки, комментарии переписаны точно. Блокирующих и критических находок нет. Единственное содержательное замечание (не блокер): сняв окно в 50 сообщений, изменение делает достижимым переполнение контекстного окна модели на длинных чатах — стоит осознанно решить, нужен ли токен-бюджет (см. раздел Architecture).[warning][stability] Рассмотреть токен-бюджетный трим истории перед
streamText, иначе длинный чат может стать неработоспособным —apps/server/src/core/ai-chat/ai-chat.service.ts:323-336.Путь
findAllByChat→history.map(rowToUiMessage)→await convertToModelMessages→streamText({ messages })не содержит никакого контроля токенов на входе: есть только компакция отдельных tool-output (compactToolOutput) и пост-фактумcontextTokensиз usage вonFinish;maxOutputTokensнамеренно не задан и ограничивает лишь вывод. Раньше окноfindRecent(…, 50)держало вход небольшим. Теперь, как только суммарный транскрипт превысит контекстное окно модели, провайдер начнёт отклонять запрос (context_length_exceeded) на каждом ходу:onErrorлишь фиксирует ход как'error'и не делает ретрай с тримом, а следующий ход снова грузит весь (уже больший) транскрипт — чат остаётся неработоспособным до создания нового. На клиенте это уже отлавливается (apps/client/.../error-message.tsпоказывает «…exceeded the model's context window. Start a new chat…»), то есть деградация управляемая — «начни новый чат», а не тихий краш. Колпак в 5000 строк — защита по памяти, а не по токенам, и срабатывает гораздо позже реального лимита окна. Это осознанный компромисс заявленной цели PR (модель видит весь транскрипт), поэтому не блокер.Fix: перед
streamTextобрезатьmessagesпод входной бюджет модели (с самого старого конца),[suggestion][stability/perf] Учитывать, что пересборка истории теперь стоит O(N) на каждый ход —
apps/server/src/core/ai-chat/ai-chat.service.ts:330-336.Каждый ход перезапрашивает все непустые строки чата (до 5000), маппит
rowToUiMessageи делаетawait convertToModelMessagesпо всему набору; раньше это было ограничено 50. Стоимость растёт линейно с длиной чата (включая крупные jsonbmetadata.parts). На реальных длинных чатах это измеримая, но вторичная по сравнению с латентностью модели нагрузка. Fix: действий не требуется, если латентность приемлема; иначе — кэшировать сконвертированную историю и дописывать только новые ходы, или ограничить загрузку токен/строчным бюджетом (то же решение, что и выше).Test coverage
Поведение, на которое опирается новый путь, уже покрыто:
findAllByChat— тот же метод, что использует экспорт, и его контракт (хронологический порядок oldest→newest, кап «оставить новейшие» с разворотом, вызов с дефолтным лимитом) закреплён интеграционными тестамиapps/server/test/integration/ai-chat-message-status.int-spec.ts:235-269. Новых веток внутриfindAllByChatпуть истории не вводит. УдалённыйfindRecentна merge-base собственных тестов не имел — его удаление не теряет покрытия.handle()не закреплено тестом —apps/server/src/core/ai-chat/ai-chat.service.ts:330-333. Симметричный экспорт-путь пинуется (ai-chat.controller.export.spec.ts:66проверяет точный вызовfindAllByChat('c1','ws1')), а уhandle()такой проверки нет, и сам метод не покрыт ни unit-, ни интеграционными тестами. Это лишь suggestion, потому чтоhandle()— стриминговый метод, который проект сам помечает как «not unit-testable» (ai-chat.service.spec.ts:500) и по соглашению не тестирует, а нижележащее поведение уже покрыто. Fix (опционально): интеграционный тест, засевающий чат >50 сообщений и проверяющий, что пересобранная история отражает все ходы.Code review (re-review) — PR #202: загрузка полного транскрипта в историю модели (отказ от окна 50 сообщений)
Вердикт: Approve. Прошлое непокрытое замечание (переполнение контекста модели на длинном чате) осознанно отложено и задокументировано в CHANGELOG, а новый интеграционный тест закрепляет ключевую гарантию перехода с окна на полный транскрипт. Дельта корректна, новых блокеров нет.
Ре-ревью дельты
286ab775..4ff5f86d(2 файлов, +41/−0). Аспекты: stability, conventions, documentation, test-coverage (параллельные ревьюеры + judge).Статус прошлых блокеров
streamText. Закрыт по существу как осознанный defer, а не косметически. Дельта не вводит token-budget trim, но явно документирует поведение вCHANGELOG.md: на очень длинном чате это «can eventually reach the model's context window; the client already surfaces that as "start a new chat"». Память процесса защищена бэкстопомFIND_ALL_BY_CHAT_LIMIT = 5000вfindAllByChat(при переполнении сохраняются новейшие строки + warning) — это явно описано как safety net, а не разговорный лимит. Решение «отложить trim» обосновано и зафиксировано.Must fix before merge
Нет.
Non-blocking
Нет.
Test coverage
Покрыто. Новый тест
default findAllByChat returns the FULL transcript past 50 rows(apps/server/test/integration/ai-chat-message-status.int-spec.ts:271) закрепляет поведенческую гарантию, на которую опирается переход вai-chat.service.ts:330(findAllByChat(chatId, workspaceId)без аргумента limit → дефолт 5000): чат из 60 строк возвращается целиком в хронологическом порядке (oldest→newest), первая репликаmsg-0, которую старое окно 50 отбросило бы, присутствует. Тест использует те же хелперы (createChat,createMessageсcreatedAt) и тот же вызов API, что и существующие тесты файла; дефолт limit и chronological-order поведение совпадают с реальной реализацией репозитория. Бэкстоп-усечение (limit=2/100) уже покрыто соседним тестом#183 review.4ff5f86d99toe99c00a9ee