d4b21957dc
Вся персистентная история реплеится провайдеру КАЖДЫЙ ход, поэтому длинный чат рано или поздно упирается в контекстное окно и получает провайдерский 400 на каждом ходу — навсегда (чат «кирпичится»). Бюджетер ограничивает РЕПЛЕЙ (никогда не мутирует персист — в БД остаётся полная запись), детерминированно и byte-stable (обрезанный префикс идентичен от хода к ходу → дружелюбно к prompt-cache). Единый оценщик chars/2.5 (кириллица; chars/4 занижает вдвое) вынесен в shared-пакет packages/token-estimate; клиентский count-stream-tokens.ts переведён на него ТЕМ ЖЕ коммитом (два расходящихся оценщика = «бейдж 60%, а бюджет уже режет»). history-budget.ts (чистый, покрыт тестами): - resolveReplayBudget(raw): min(100k, 0.7×window) при заданном окне; флэт 100k при незаданном (именно эти инсталляции ловят терминальный overflow — warn-лог); 0 = явный off-switch. Читается СЫРОЙ chatContextWindow, т.к. parsePositiveInt схлопывает 0 и unset в undefined (новое поле ResolvedAiConfig.chatContextWindowRaw). - trimHistoryForReplay: первичный сигнал — провайдерский факт metadata.contextTokens прошлого хода; chars-оценка — дельта/раскройка/фолбэк. Порядок: обрезка tool-outputs старых ходов (head+tail+маркер) → механическое схлопывание старейших ходов (конкатенация, НЕ LLM) → текущий + последние N ходов всегда полные. Пейринг tool-call/result сохраняется (схлопывание убирает ОБЕ части). - isContextOverflowError: классификация провайдерского 400 (статус + паттерны). Реактивная ветка: превентивная оценка не даёт инварианта (первый переполняющий ход не имеет usage). onError классифицирует context-overflow → пишет различимую причину и штампует metadata.replayOverflow; следующий ход бюджетер режет агрессивно (0.5×), что и раскирпичивает чат. Наблюдаемость: metadata.replayTrimmedToTokens. ПРИМЕЧАНИЕ по реактивной ветке (форк, требует решения ревьюера): истинный in-turn re-pipe (перезапуск streamText в тот же ответ) архитектурно несовместим с текущим пайпом — pipeUIMessageStreamToResponse пишет writeHead СИНХРОННО (подтверждено в ai@6.0.207), а suite ожидает await stream() c моком, не дёргающим колбэки, — так что отложенный пайп/ожидание сигнала повесит тесты. Поэтому реализована реактивная рекавери «классификация → штамп → агрессивный ре-трим на следующем ходу», что даёт тот же инвариант (чат не кирпичится) без рискованного рефактора стрима. Тесты (наблюдаемые свойства): объём записи через дельту pg_current_wal_lsn() на живой gitmost-test-pg вокруг 50-шагового прогона (несжимаемые payload'ы) — trace-колонка v1=140МБ → v2=0.04МБ (в 3206× меньше), полная строка 289МБ → 140МБ (−51%); dual-shape не нужен здесь; «окно не задано → бюджет применяется»; реактивная классификация на реальном 400-шейпе; parity клиент/сервер оценщика. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 lines
368 B
JSON
17 lines
368 B
JSON
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"module": "CommonJS",
|
|
"moduleResolution": "Node",
|
|
"outDir": "./dist",
|
|
"rootDir": "./src",
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"declaration": true
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["src/**/*.test.ts"]
|
|
}
|