Стабилизация, итерация 5: дисциплина данных — дедуп tool-outputs, токен-бюджет реплея (один PR, 6 коммитов) #490

Open
opened 2026-07-11 02:03:56 +03:00 by agent_vscode · 0 comments
Collaborator

Один PR, коммиты по порядку. Жёстких зависимостей от других итераций нет. Координаты — develop@363f20ab. Контекст масштаба: сейчас каждый onStepFinish переписывает ВСЮ assistant-строку, причём каждый tool-output хранится ДВАЖДЫ (в metadata.parts через assistantParts, service:1997, И в tool_calls через serializeSteps, :2267) → 50-шаговый ран с outputs по 50–200 KB = 127–510 МБ записи в Postgres за один ран (+WAL/TOAST, dead tuples) и 50–100 мс блокировок event loop на поздних шагах; вся история (кап 5000 СТРОК, не токенов) реплеится провайдеру каждый ход → длинный чат терминально умирает об контекст-окно (провайдерский 400 на каждом ходе, навсегда).

Коммит 1 — perf(ai-chat): формат трейса tool_calls v2 — outputs только в parts

Новый формат элементов tool_calls: {toolName, input} для вызова; {toolName, ok: true} для успеха; {toolName, error, kind: 'thrown' | 'interrupted'} для отказа. kind обязателен: синтетический «Tool call did not complete.» при Stop иначе неотличим от реального hard-fail и загрязняет error-rate (различие структурное — errorsById vs синтетическая ветка, НЕ per-tool классификация). Soft-failure маркеры в трейс НЕ выносятся (per-tool классификатор = ручное зеркало, его ошибка персистится навсегда; soft-анализ — запросами по metadata.parts). Back-compat: старые строки НЕ мигрируются (перезапись гигантских jsonb = тот самый WAL-чарн); маркер эры metadata.toolTraceVersion: 2; диагностические запросы в docs/reading-ai-logs.md переписываются dual-shape с ветвлением по маркеру + правило «агрегаты/тренды не сравнивать через границу эр» (скачок метрик на неделе перехода — артефакт). UI action-log и markdown-экспорт читают только parts — не затронуты (регресс-пин).

Коммит 2 — perf(ai-chat): кэш compactToolOutput по identity шага

compactToolOutput сейчас делает JSON.stringify каждого output на КАЖДОМ flush (:1868-1885). Мемоизация по identity шага — один stringify на шаг.

Коммит 3 — feat(ai-chat): токен-бюджет реплея истории + реактивная ветка

  1. Первичный сигнал — провайдерский факт: metadata.contextTokens последнего хода (уже персистится, :1331-1337) против порога; chars-оценка — только для дельты новых сообщений, раскройки что резать, и фолбэк для чатов без usage.
  2. Порог: min(100k, 0.7 × chatContextWindow) при заданном окне; флэт 100k при незаданном (chatContextWindow — optional-поле: бюджетер, выключенный по умолчанию, не защищает ровно те инсталляции, что ловят терминальный overflow); 0 = явный off-switch; warn-лог при срабатывании дефолта.
  3. Оценщик chars/2.5 (кириллица; chars/4 занижает вдвое) — в shared-модуле packages/; клиентский count-stream-tokens.ts (chars/4) переводится на тот же модуль ТЕМ ЖЕ коммитом (два расходящихся оценщика = «бейдж показывает 60%, а бюджет уже режет»).
  4. Локус: replay-time, персист не мутируется; мемоизация по (row id, updatedAt); порядок: обрезка tool-outputs СТАРЫХ ходов (head+tail + маркер «truncated for replay; call the tool again») → механическое схлопывание старейших ходов (конкатенация, НЕ LLM); текущий + последние N ходов всегда полные; усечение детерминированное и byte-stable от хода к ходу (иначе ломается provider prompt-cache — деньги на длинных чатах).
  5. Реактивная ветка (обязательна): классифицировать провайдерский context-overflow (400 + паттерны сообщения) → агрессивный ре-трим → ОДИН in-turn ретрай. Превентивная оценка не даёт инварианта (первый переполняющий ход не имеет usage) — только реактивная ветка гарантирует «чат не кирпичится».
  6. Наблюдаемость: решение бюджетера в metadata хода («replay truncated at N tokens») — UI может показать.

Коммит 4 — perf(ai-chat): deferred-активация тулов в metadata чата

ai-chat.service.ts:1054: активированный set сбрасывается каждый ход → loadTools-round-trip заново. Хранить в metadata чата; при загрузке пересекать с актуальным validDeferredNames (смена allowlist/ролей — иначе prepareAgentStep получит имена несуществующих тулов).

Коммит 5 — perf(ai-chat): snapshotOpenPage fast-path

ai-chat.service.ts:684-717: полный экспорт+upsert каждый ход. Fast-path по updatedAt на запись (на чтении уже есть).

Коммит 6 — perf(mcp): checkNewComments — параллелизм

packages/mcp/src/client.ts:3544: O(N) последовательных REST-вызовов по страницам working set. Параллелизм с капом 5–8 (серверный batch-эндпоинт «comments updated since T по space» — опционально, отдельным заходом).

Тесты (наблюдаемые свойства, не моки): объём записи через дельту pg_current_wal_lsn() на живой БД вокруг 50-шагового прогона, assert O(Σ шагов) — НЕ «байты через мок-репо» (мок меряет ровно то, что не болит); dual-shape запросы на фикстурах обеих эр; «окно не задано → бюджет применяется»; реактивная ветка с реальным 400-шейпом провайдера; parity-тест единого оценщика клиент/сервер.

Один PR, коммиты по порядку. Жёстких зависимостей от других итераций нет. Координаты — develop@363f20ab. Контекст масштаба: сейчас каждый `onStepFinish` переписывает ВСЮ assistant-строку, причём каждый tool-output хранится ДВАЖДЫ (в `metadata.parts` через `assistantParts`, service:1997, И в `tool_calls` через `serializeSteps`, :2267) → 50-шаговый ран с outputs по 50–200 KB = **127–510 МБ записи в Postgres за один ран** (+WAL/TOAST, dead tuples) и 50–100 мс блокировок event loop на поздних шагах; вся история (кап 5000 СТРОК, не токенов) реплеится провайдеру каждый ход → длинный чат терминально умирает об контекст-окно (провайдерский 400 на каждом ходе, навсегда). ### Коммит 1 — perf(ai-chat): формат трейса tool_calls v2 — outputs только в parts Новый формат элементов `tool_calls`: `{toolName, input}` для вызова; `{toolName, ok: true}` для успеха; `{toolName, error, kind: 'thrown' | 'interrupted'}` для отказа. `kind` обязателен: синтетический «Tool call did not complete.» при Stop иначе неотличим от реального hard-fail и загрязняет error-rate (различие структурное — errorsById vs синтетическая ветка, НЕ per-tool классификация). **Soft-failure маркеры в трейс НЕ выносятся** (per-tool классификатор = ручное зеркало, его ошибка персистится навсегда; soft-анализ — запросами по `metadata.parts`). Back-compat: старые строки НЕ мигрируются (перезапись гигантских jsonb = тот самый WAL-чарн); маркер эры `metadata.toolTraceVersion: 2`; диагностические запросы в docs/reading-ai-logs.md переписываются dual-shape с ветвлением по маркеру + правило «агрегаты/тренды не сравнивать через границу эр» (скачок метрик на неделе перехода — артефакт). UI action-log и markdown-экспорт читают только parts — не затронуты (регресс-пин). ### Коммит 2 — perf(ai-chat): кэш compactToolOutput по identity шага `compactToolOutput` сейчас делает JSON.stringify каждого output на КАЖДОМ flush (:1868-1885). Мемоизация по identity шага — один stringify на шаг. ### Коммит 3 — feat(ai-chat): токен-бюджет реплея истории + реактивная ветка 1. **Первичный сигнал — провайдерский факт:** `metadata.contextTokens` последнего хода (уже персистится, :1331-1337) против порога; chars-оценка — только для дельты новых сообщений, раскройки что резать, и фолбэк для чатов без usage. 2. Порог: `min(100k, 0.7 × chatContextWindow)` при заданном окне; **флэт 100k при незаданном** (`chatContextWindow` — optional-поле: бюджетер, выключенный по умолчанию, не защищает ровно те инсталляции, что ловят терминальный overflow); `0` = явный off-switch; warn-лог при срабатывании дефолта. 3. Оценщик `chars/2.5` (кириллица; chars/4 занижает вдвое) — в **shared-модуле packages/**; клиентский `count-stream-tokens.ts` (chars/4) переводится на тот же модуль ТЕМ ЖЕ коммитом (два расходящихся оценщика = «бейдж показывает 60%, а бюджет уже режет»). 4. Локус: **replay-time, персист не мутируется**; мемоизация по (row id, updatedAt); порядок: обрезка tool-outputs СТАРЫХ ходов (head+tail + маркер «truncated for replay; call the tool again») → механическое схлопывание старейших ходов (конкатенация, НЕ LLM); текущий + последние N ходов всегда полные; усечение детерминированное и byte-stable от хода к ходу (иначе ломается provider prompt-cache — деньги на длинных чатах). 5. **Реактивная ветка (обязательна):** классифицировать провайдерский context-overflow (400 + паттерны сообщения) → агрессивный ре-трим → ОДИН in-turn ретрай. Превентивная оценка не даёт инварианта (первый переполняющий ход не имеет usage) — только реактивная ветка гарантирует «чат не кирпичится». 6. Наблюдаемость: решение бюджетера в metadata хода («replay truncated at N tokens») — UI может показать. ### Коммит 4 — perf(ai-chat): deferred-активация тулов в metadata чата `ai-chat.service.ts:1054`: активированный set сбрасывается каждый ход → loadTools-round-trip заново. Хранить в metadata чата; **при загрузке пересекать с актуальным validDeferredNames** (смена allowlist/ролей — иначе prepareAgentStep получит имена несуществующих тулов). ### Коммит 5 — perf(ai-chat): snapshotOpenPage fast-path `ai-chat.service.ts:684-717`: полный экспорт+upsert каждый ход. Fast-path по `updatedAt` на запись (на чтении уже есть). ### Коммит 6 — perf(mcp): checkNewComments — параллелизм `packages/mcp/src/client.ts:3544`: O(N) последовательных REST-вызовов по страницам working set. Параллелизм с капом 5–8 (серверный batch-эндпоинт «comments updated since T по space» — опционально, отдельным заходом). **Тесты (наблюдаемые свойства, не моки):** объём записи через дельту `pg_current_wal_lsn()` на живой БД вокруг 50-шагового прогона, assert O(Σ шагов) — НЕ «байты через мок-репо» (мок меряет ровно то, что не болит); dual-shape запросы на фикстурах обеих эр; «окно не задано → бюджет применяется»; реактивная ветка с реальным 400-шейпом провайдера; parity-тест единого оценщика клиент/сервер.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#490