fix(ai-chat): реактивная рекавери — итеративная эскалация 0.5^k + пол вместо фикс. 0.5× (#520) #546

Merged
vvzvlad merged 2 commits from fix/520-recovery-escalation into develop 2026-07-12 18:56:51 +03:00
Collaborator

Summary

Follow-up к #490/#510: реактивная рекавери после context-overflow 400 была фиксированной долей 0.5× от булева replayOverflow — не эскалировала. При НЕзаданном chatContextWindow (плоский дефолт 100k) и реально малом окне модели (<50k) даже 0.5×100k=50k переполняло снова, ход перештамповывался, но булев был уже true → второго ужесточения нет. Остаточный кирпич: чат застревал на 50k каждый ход. closes #520.

Фикс (итеративная эскалация + пол)

  • Сигнал boolean replayOverflow → счётчик последовательных overflow metadata.replayOverflowCount (k). Инкремент: priorOverflowCount + 1 (стрик читается в начале хода). Сброс: чистый finalize опускает поле → читатель берёт самый свежий assistant-row → отсутствие = k=0 (авто-сброс). Back-compat: legacy replayOverflow:true без счётчика → k=1 (старое 0.5× для in-flight чатов); отрицательное/NaN → 0.
  • resolveEffectiveReplayThreshold(threshold, k) = max(floor(threshold · 0.5^k), min(REPLAY_MIN_FLOOR_TOKENS, threshold)). k=1→0.5×, k=2→0.25×, … сходится/клэмпится на REPLAY_MIN_FLOOR_TOKENS = 8_000. min(floor, threshold) — чтобы пол НЕ поднимал легитимно малый сконфигурированный бюджет выше себя (иначе переполнил бы своё же окно). null/k=0 → passthrough (off-switch не переопределяется).

How verified (реальный pg)

  • history-budget 19; ai-chat.service+run-race 137; write-volume int-spec (jsonb lifecycle 1→2→3, reset→0, legacy→1) 3 — все зелёные.
  • Регресс-тест сходимости: base=100k, realWindow=40k → утверждает СХОДИМОСТЬ ([2,3,4,5].some(k → threshold<40k)), а не «≠50k». Мутация **k → **1 краснит 5 тестов (un-bricks + floor-clamp).
  • #490-семантика не ослаблена (overflow штампует count=1, non-overflow — ничего); isContextOverflowError/resolveReplayBudget/keep-recent-turns не тронуты.
  • Внутренний цикл: реализация → адверсариальный ревью (жизненный цикл счётчика + сходимость, мутация-подтверждена) → APPROVE. node_modules 0; scope apps/server/src/core/ai-chat/.

Осознанный трейд-офф (INFO из ревью)

Маленький сконфигурированный бюджет (threshold<8k) эскалацией ниже себя не тянется (по замыслу — пол не должен переполнять собственное окно). Остаточный кейс, не дефект.

Checklist

  • критерии приёмки из #520 выполнены
  • вне scope ничего не менялось; node_modules нет
## Summary Follow-up к #490/#510: реактивная рекавери после context-overflow 400 была фиксированной долей `0.5×` от булева `replayOverflow` — не эскалировала. При НЕзаданном `chatContextWindow` (плоский дефолт 100k) и реально малом окне модели (<50k) даже `0.5×100k=50k` переполняло снова, ход перештамповывался, но булев был уже `true` → второго ужесточения нет. Остаточный кирпич: чат застревал на 50k каждый ход. closes #520. ## Фикс (итеративная эскалация + пол) - Сигнал boolean `replayOverflow` → счётчик **последовательных** overflow `metadata.replayOverflowCount` (k). Инкремент: `priorOverflowCount + 1` (стрик читается в начале хода). Сброс: чистый finalize опускает поле → читатель берёт самый свежий assistant-row → отсутствие = k=0 (авто-сброс). Back-compat: legacy `replayOverflow:true` без счётчика → k=1 (старое 0.5× для in-flight чатов); отрицательное/NaN → 0. - `resolveEffectiveReplayThreshold(threshold, k) = max(floor(threshold · 0.5^k), min(REPLAY_MIN_FLOOR_TOKENS, threshold))`. k=1→0.5×, k=2→0.25×, … сходится/клэмпится на **`REPLAY_MIN_FLOOR_TOKENS = 8_000`**. `min(floor, threshold)` — чтобы пол НЕ поднимал легитимно малый *сконфигурированный* бюджет выше себя (иначе переполнил бы своё же окно). `null`/k=0 → passthrough (off-switch не переопределяется). ## How verified (реальный pg) - history-budget **19**; ai-chat.service+run-race **137**; write-volume int-spec (jsonb lifecycle 1→2→3, reset→0, legacy→1) **3** — все зелёные. - Регресс-тест сходимости: `base=100k, realWindow=40k` → утверждает СХОДИМОСТЬ (`[2,3,4,5].some(k → threshold<40k)`), а не «≠50k». Мутация `**k → **1` краснит 5 тестов (un-bricks + floor-clamp). - #490-семантика не ослаблена (overflow штампует count=1, non-overflow — ничего); `isContextOverflowError`/`resolveReplayBudget`/keep-recent-turns не тронуты. - Внутренний цикл: реализация → адверсариальный ревью (жизненный цикл счётчика + сходимость, мутация-подтверждена) → APPROVE. node_modules 0; scope `apps/server/src/core/ai-chat/`. ## Осознанный трейд-офф (INFO из ревью) Маленький *сконфигурированный* бюджет (threshold<8k) эскалацией ниже себя не тянется (по замыслу — пол не должен переполнять собственное окно). Остаточный кейс, не дефект. ## Checklist - [x] критерии приёмки из #520 выполнены - [x] вне scope ничего не менялось; node_modules нет
agent_coder added the review/needs label 2026-07-12 05:09:59 +03:00
Collaborator

Ревью — эскалация рекавери ai-chat (#520). Вердикт: needs-human

Весь PR корректен и хорошо покрыт (крюкс-счётчик, математика, back-compat, обычный чат — всё сверено + гейт зелёный: unit 156/156, int 3/3 на реальном pg, 3 мутации убиты). Открыт ОДИН вопрос — продуктовая развилка, которую я не должен решать сам: как рекавери ведёт себя при МАЛОМ настроенном окне модели. По умолчанию (окно не настроено) всё идеально; вопрос только про явно сконфигурированные окна < ~11 400 токенов.

Контекст (простыми словами)

Когда ai-chat отправляет модели слишком длинную историю, модель отвечает ошибкой «context overflow». Этот PR чинит баг #520: раньше рекавери один раз обрезала историю до 0.5× бюджета и, если и это не влезало, чат ЗАСТРЕВАЛ навсегда («кирпич»). Теперь при каждом ПОДРЯД overflow'е обрезка усиливается: бюджет × 0.5^k (k = число overflow'ов подряд), с нижним полом. Для дефолтного (ненастроенного) окна 100k всё работает: 100k→50k→25k→…→8k, пока не влезет, потом чистый ход сбрасывает k. Это правильно.

Главный вопрос

Нижний пол задан как min(8000, настроенный_бюджет). Для НАСТРОЕННОГО окна модели меньше ~11 400 токенов (бюджет = 0.7×окно < 8000) пол становится равен самому бюджету → рекавери вообще перестаёт резать, при любом k. Два следствия:

  1. Регрессия vs то, что сейчас в develop: старый код на overflow ВСЕГДА резал до 0.5× (например бюджет 5600 → 2800). Новый оставляет 5600 (не режет). Т.е. для малых окон этот PR — «фикс кирпича» — делает рекавери СЛАБЕЕ, чем было.
  2. Возвращает ровно тот кирпич #520 для малых моделей: админ ставит chatContextWindow=8000 (реальная 8k-модель) → бюджет 5600; если 5600 токенов + системный промпт + схемы инструментов не влезают в 8k, старый код резал до 2800 и восстанавливался, новый застревает на 5600 навсегда.

Автор ОСОЗНАННО закодил и покрыл тестом «никогда не резать ниже настроенного бюджета» (history-budget.spec.ts:67 «never inflates a small configured budget above itself»). Поэтому это не явный баг-с-очевидным-фиксом, а решение о контракте: настроенный админом бюджет — это ЖЁСТКИЙ пол или нет? Я не могу решить за продукт (нужно знать, поддерживает ли gitmost малоконтекстные модели).

Варианты
Суть Плюсы Минусы
A (как сейчас) Настроенный бюджет — жёсткий пол, ниже не резать Уважает конфиг админа; просто Регрессия vs develop; НАВСЕГДА бриктует чаты на моделях с реальным окном ≤ ~11k токенов
B Разрешить рекавери резать НИЖЕ настроенного бюджета до малого абсолютного минимума (напр. 2k) при повторных overflow Восстанавливает un-brick для малых окон — собственно цель #520 Рекавери может обрезать ниже, чем настроил админ

Рекомендация: как минимум НЕ регрессировать — сделать пол настоящим АБСОЛЮТНЫМ минимумом, а не «= настроенному бюджету» (напр. Math.max(scaled, Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(threshold*0.5)))), чтобы для базы<8000 сохранился хотя бы старый 0.5×-рез (спека «не РАЗДУВАТЬ малый бюджет» при этом остаётся выполнена — раздувание и так не происходит). Это ≈ Вариант B с полом ~половина-бюджета. Выбирай A, только если окна < ~11.4k явно вне scope #520 — тогда стоит задокументировать, что chatContextWindow должен быть ≥ ~11 400.

Объективка зелёная, весь остальной PR верен — открыт только этот выбор. Loop остановлен до твоего решения.

## Ревью — эскалация рекавери ai-chat (#520). Вердикт: **needs-human** Весь PR **корректен и хорошо покрыт** (крюкс-счётчик, математика, back-compat, обычный чат — всё сверено + гейт зелёный: unit 156/156, int 3/3 на реальном pg, 3 мутации убиты). Открыт ОДИН вопрос — продуктовая развилка, которую я не должен решать сам: как рекавери ведёт себя при МАЛОМ настроенном окне модели. По умолчанию (окно не настроено) всё идеально; вопрос только про явно сконфигурированные окна < ~11 400 токенов. ### Контекст (простыми словами) Когда ai-chat отправляет модели слишком длинную историю, модель отвечает ошибкой «context overflow». Этот PR чинит баг #520: раньше рекавери один раз обрезала историю до 0.5× бюджета и, если и это не влезало, чат ЗАСТРЕВАЛ навсегда («кирпич»). Теперь при каждом ПОДРЯД overflow'е обрезка усиливается: бюджет × 0.5^k (k = число overflow'ов подряд), с нижним полом. Для дефолтного (ненастроенного) окна 100k всё работает: 100k→50k→25k→…→8k, пока не влезет, потом чистый ход сбрасывает k. Это правильно. ### Главный вопрос Нижний пол задан как `min(8000, настроенный_бюджет)`. Для НАСТРОЕННОГО окна модели меньше ~11 400 токенов (бюджет = 0.7×окно < 8000) пол становится равен самому бюджету → рекавери **вообще перестаёт резать**, при любом k. Два следствия: 1. **Регрессия vs то, что сейчас в develop:** старый код на overflow ВСЕГДА резал до 0.5× (например бюджет 5600 → 2800). Новый оставляет 5600 (не режет). Т.е. для малых окон этот PR — «фикс кирпича» — делает рекавери СЛАБЕЕ, чем было. 2. **Возвращает ровно тот кирпич #520** для малых моделей: админ ставит `chatContextWindow=8000` (реальная 8k-модель) → бюджет 5600; если 5600 токенов + системный промпт + схемы инструментов не влезают в 8k, старый код резал до 2800 и восстанавливался, новый застревает на 5600 навсегда. Автор ОСОЗНАННО закодил и покрыл тестом «никогда не резать ниже настроенного бюджета» (`history-budget.spec.ts:67` «never inflates a small configured budget above itself»). Поэтому это не явный баг-с-очевидным-фиксом, а решение о контракте: настроенный админом бюджет — это ЖЁСТКИЙ пол или нет? Я не могу решить за продукт (нужно знать, поддерживает ли gitmost малоконтекстные модели). <details> <summary>Варианты</summary> | | Суть | Плюсы | Минусы | |---|---|---|---| | **A (как сейчас)** | Настроенный бюджет — жёсткий пол, ниже не резать | Уважает конфиг админа; просто | Регрессия vs develop; НАВСЕГДА бриктует чаты на моделях с реальным окном ≤ ~11k токенов | | **B** | Разрешить рекавери резать НИЖЕ настроенного бюджета до малого абсолютного минимума (напр. 2k) при повторных overflow | Восстанавливает un-brick для малых окон — собственно цель #520 | Рекавери может обрезать ниже, чем настроил админ | **Рекомендация:** как минимум НЕ регрессировать — сделать пол настоящим АБСОЛЮТНЫМ минимумом, а не «= настроенному бюджету» (напр. `Math.max(scaled, Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(threshold*0.5)))`), чтобы для базы<8000 сохранился хотя бы старый 0.5×-рез (спека «не РАЗДУВАТЬ малый бюджет» при этом остаётся выполнена — раздувание и так не происходит). Это ≈ Вариант B с полом ~половина-бюджета. Выбирай A, только если окна < ~11.4k явно вне scope #520 — тогда стоит задокументировать, что `chatContextWindow` должен быть ≥ ~11 400. </details> Объективка зелёная, весь остальной PR верен — открыт только этот выбор. Loop остановлен до твоего решения.
agent_reviewer added needs-human and removed review/needs labels 2026-07-12 07:05:04 +03:00
Collaborator

Решение по эскалации (владелец): Вариант B — рекавери МОЖЕТ резать ниже явно настроенного бюджета при повторных overflow. Пол — абсолютный минимум по формуле ревьюера: Math.max(scaled, Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(threshold * 0.5))) — для малых окон гарантирован как минимум прежний 0.5×-рез, эскалация 0.5^k продолжается ниже бюджета до пола.

Обоснование: «чат не кирпичится на overflow» — инвариант реактивной ветки (спека эпика #497/итерация 5), а не best-effort; настроенное окно — утверждение о ёмкости модели, не обещание размера реплея; когда провайдер фактом 400-ки доказал несоответствие, реальность бьёт конфиг. Противоречия с решением по #510 (опция A) нет: там верхняя граница ШТАТНОГО пути (уважать конфиг, пока влезает), здесь — выживание при доказанном невлезании.

К исполнению вместе с B:

  1. Перепиннить тест history-budget.spec.ts:67 («never inflates a small configured budget above itself» — само свойство «не раздувать» сохраняется, но кейс «жёсткий пол = бюджет» после B стал бы подгонкой).
  2. Warn-лог + пометка в metadata хода при резе НИЖЕ настроенного бюджета (принцип наблюдаемости бюджетера — админ должен видеть, что его конфиг фактически не помещается, и поправить окно).
  3. Тест: настроенное окно 8000 (бюджет 5600) + повторные overflow → k=1 режет до 2800, k=2 до 1400... до пола; чистый ход сбрасывает k. Mutation: вернуть пол=бюджет → красный.
Решение по эскалации (владелец): **Вариант B** — рекавери МОЖЕТ резать ниже явно настроенного бюджета при повторных overflow. Пол — абсолютный минимум по формуле ревьюера: `Math.max(scaled, Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(threshold * 0.5)))` — для малых окон гарантирован как минимум прежний 0.5×-рез, эскалация 0.5^k продолжается ниже бюджета до пола. Обоснование: «чат не кирпичится на overflow» — инвариант реактивной ветки (спека эпика #497/итерация 5), а не best-effort; настроенное окно — утверждение о ёмкости модели, не обещание размера реплея; когда провайдер фактом 400-ки доказал несоответствие, реальность бьёт конфиг. Противоречия с решением по #510 (опция A) нет: там верхняя граница ШТАТНОГО пути (уважать конфиг, пока влезает), здесь — выживание при доказанном невлезании. К исполнению вместе с B: 1. Перепиннить тест `history-budget.spec.ts:67` («never inflates a small configured budget above itself» — само свойство «не раздувать» сохраняется, но кейс «жёсткий пол = бюджет» после B стал бы подгонкой). 2. Warn-лог + пометка в metadata хода при резе НИЖЕ настроенного бюджета (принцип наблюдаемости бюджетера — админ должен видеть, что его конфиг фактически не помещается, и поправить окно). 3. Тест: настроенное окно 8000 (бюджет 5600) + повторные overflow → k=1 режет до 2800, k=2 до 1400... до пола; чистый ход сбрасывает k. Mutation: вернуть пол=бюджет → красный.
agent_vscode added review/changes-requested and removed needs-human labels 2026-07-12 17:33:01 +03:00
Author
Collaborator

Реализована Опция B (решение владельца) — на ревью

Реализовал вариант B: рекавери может резать реплей-бюджет НИЖЕ настроенного при повторных overflow.

Формула пола (history-budget.ts, resolveEffectiveReplayThreshold):

const scaled = Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION ** k);
const floor = Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION));
return Math.max(scaled, floor);

Пол сменён со старого min(REPLAY_MIN_FLOOR_TOKENS, thresholdTokens) (пиннился к бюджету) на min(REPLAY_MIN_FLOOR_TOKENS, floor(0.5×budget)). REPLAY_MIN_FLOOR_TOKENS уже существовал = 8000, переиспользован (новую константу не вводил). threshold здесь = производный реплей-бюджет (floor(0.7×окно)), не окно.

Что реально чинится: для МАЛЫХ окон (budget ≤ 8000, т.е. окно ≤ ~11400 — ровно кейс из needs-human ревьюера) старый пол = min(8000, 5600) = 5600 = бюджет → эскалация упиралась В бюджет и НЕ резала ниже → кирпич #520. Новый пол = 2800 → расклинивает (гарантирован как минимум прежний 0.5×-рез). Для БОЛЬШИХ окон (budget ≥ 16000) поведение идентично старому (пол так и 8000).

Наблюдаемость: replayBelowConfiguredBudget — warn-лог (настроенный бюджет + фактический + k) при резе ниже бюджета, и флаг в metadata хода (штампуется только когда реально ниже). Сброс k на чистом ходу сохранён.

Тесты: перепинен history-budget.spec.ts:67 (инвариант «не раздувает выше себя» жив; убрано допущение floor==budget), + escalation-below-budget (окно 8000→бюджет 5600→k≥1 режет до пола 2800; чистый ход сбрасывает k) + large-window спуск до 8000 + observability. 151/151. Мутации: пол→budget краснит below-budget тесты (large-window зелёный — подтверждает, что дельта бьёт по малым окнам); снятие metadata-штампа краснит observability-тест. Отребейзено на develop (61 коммит, конфликтов нет).

Два INFO на подтверждение (@vvzvlad):

  1. Формула vs твой нарратив. Для окна 8000 / бюджета 5600 пол = min(8000, 2800) = 2800, так что спуск ОДНОШАГОВЫЙ: k=1→2800, k≥2 остаётся 2800 (пол достигнут). Многошаговый спуск («k=2→1400…») из твоего описания достижим только для окон > ~22857 (где 0.5×budget > 8000). Я следовал ФОРМУЛЕ (она авторитетна), числа примера чуть расходятся с ней — это ожидаемо, не баг.
  2. Значение пола. REPLAY_MIN_FLOOR_TOKENS = 8000 — подтверди, что это желаемый абсолютный пол. Менять его значение сам не стал (сдвинет пол сходимости для всех больших окон).

внутренний цикл: 1 проход внутреннего ревью — APPROVE, threshold-связывание корректно, недостижимость нарратива = следствие формулы, наблюдаемость/сброс целы, мутации хирургичны.

Новый head: rebased.

## Реализована Опция B (решение владельца) — на ревью Реализовал вариант B: рекавери может резать реплей-бюджет НИЖЕ настроенного при повторных overflow. **Формула пола** (`history-budget.ts`, `resolveEffectiveReplayThreshold`): ```ts const scaled = Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION ** k); const floor = Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION)); return Math.max(scaled, floor); ``` Пол сменён со старого `min(REPLAY_MIN_FLOOR_TOKENS, thresholdTokens)` (пиннился к бюджету) на `min(REPLAY_MIN_FLOOR_TOKENS, floor(0.5×budget))`. `REPLAY_MIN_FLOOR_TOKENS` уже существовал = **8000**, переиспользован (новую константу не вводил). `threshold` здесь = производный реплей-бюджет (`floor(0.7×окно)`), не окно. **Что реально чинится:** для МАЛЫХ окон (budget ≤ 8000, т.е. окно ≤ ~11400 — ровно кейс из needs-human ревьюера) старый пол = `min(8000, 5600) = 5600 = бюджет` → эскалация упиралась В бюджет и НЕ резала ниже → кирпич #520. Новый пол = 2800 → расклинивает (гарантирован как минимум прежний 0.5×-рез). Для БОЛЬШИХ окон (budget ≥ 16000) поведение идентично старому (пол так и 8000). **Наблюдаемость:** `replayBelowConfiguredBudget` — warn-лог (настроенный бюджет + фактический + k) при резе ниже бюджета, и флаг в metadata хода (штампуется только когда реально ниже). Сброс k на чистом ходу сохранён. **Тесты:** перепинен `history-budget.spec.ts:67` (инвариант «не раздувает выше себя» жив; убрано допущение floor==budget), + escalation-below-budget (окно 8000→бюджет 5600→k≥1 режет до пола 2800; чистый ход сбрасывает k) + large-window спуск до 8000 + observability. 151/151. Мутации: пол→budget краснит below-budget тесты (large-window зелёный — подтверждает, что дельта бьёт по малым окнам); снятие metadata-штампа краснит observability-тест. Отребейзено на develop (61 коммит, конфликтов нет). **Два INFO на подтверждение (@vvzvlad):** 1. **Формула vs твой нарратив.** Для окна 8000 / бюджета 5600 пол = `min(8000, 2800) = 2800`, так что спуск ОДНОШАГОВЫЙ: k=1→2800, k≥2 остаётся 2800 (пол достигнут). Многошаговый спуск («k=2→1400…») из твоего описания достижим только для окон > ~22857 (где `0.5×budget > 8000`). Я следовал ФОРМУЛЕ (она авторитетна), числа примера чуть расходятся с ней — это ожидаемо, не баг. 2. **Значение пола.** `REPLAY_MIN_FLOOR_TOKENS = 8000` — подтверди, что это желаемый абсолютный пол. Менять его значение сам не стал (сдвинет пол сходимости для всех больших окон). внутренний цикл: 1 проход внутреннего ревью — APPROVE, `threshold`-связывание корректно, недостижимость нарратива = следствие формулы, наблюдаемость/сброс целы, мутации хирургичны. Новый head: rebased.
agent_coder added 2 commits 2026-07-12 18:02:25 +03:00
Остаточный кирпич (#490/#510): при незаданном chatContextWindow база = плоский
дефолт 100k, а реальное окно модели маленькое (<50k). Фиксированный одинарный
cut 0.5×100k=50k всё равно превышал реальное окно → провайдер снова 400,
строка переставлялась replayOverflow, но булев priorOverflowed уже был true →
второго ужатия не происходило. Чат навсегда застревал на 50k и не восстанавливался.

Фикс (без парсинга тел 400):
- Сигнал из булева переведён в счётчик `metadata.replayOverflowCount` = число
  ПОДРЯД идущих overflow-ходов: инкремент (prior+1) на каждом overflow, сброс в 0
  на любом чистом финализе (чистая строка не пишет поле → читается как 0).
  BACK-COMPAT: старая строка с булевым `replayOverflow:true` читается как k=1.
- resolveEffectiveReplayThreshold(threshold, k) = max(floor(threshold·0.5**k),
  min(REPLAY_MIN_FLOOR_TOKENS, threshold)). k=0 → база; k=1 → 0.5×; k=2 → 0.25×;
  большой k → упирается в пол 8k (сходимость). null-база (trimming OFF) не трогается;
  пол никогда не поднимает легитимно малый настроенный бюджет выше него самого.
- Пол REPLAY_MIN_FLOOR_TOKENS=8k: ниже него чат не несёт осмысленный недавний
  контекст, и даже малое реальное окно его вмещает; keep-recent-turns сверху.

Тесты: таблица эскалации (k=0/1/2/большой/null), регрессия остаточного кирпича
(база 100k, окно ~40k → сходится ниже 40k, чего фиксированный 0.5× никогда не мог),
жизненный цикл счётчика на реальном pg (инкремент/сброс/back-compat через jsonb),
мутация **k→**1 краснит convergence-тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Решение владельца по #520 (эпик #497 итерация 5): при повторном overflow реактивная
рекавери ДОЛЖНА иметь право резать бюджет реплея ниже явно настроенного окна —
«чат не должен кирпичиться на overflow» это ИНВАРИАНТ реактивной ветки, а
настроенное окно — заявление о ёмкости модели, не обещание о размере реплея; когда
провайдер 400-ит, реальность бьёт конфиг. (Не конфликтует с #510 Опция A: та про
верхнюю границу НОРМАЛЬНОГО пути — уважать конфиг, пока он влезает.)

- Пол эскалации: max(scaled, min(REPLAY_MIN_FLOOR_TOKENS, floor(0.5×threshold))).
  Было min(REPLAY_MIN_FLOOR_TOKENS, threshold) — жёсткий пол на настроенном бюджете.
  Теперь БОЛЬШОЕ окно эскалирует НИЖЕ настроенного бюджета до фикс. пола 8k; МАЛОЕ
  окно (0.5×threshold < 8k) падает до floor(0.5×threshold) — минимум старый 0.5× cut
  (никогда не хуже, чем раньше), но и не поднимается выше самого бюджета.
  REPLAY_MIN_FLOOR_TOKENS=8k без изменений (константа уже была). Сброс k на чистом
  ходу сохранён (счётчик replayOverflowCount не пишется на чистом финализе).
- Наблюдаемость: warn-лог (настроенный бюджет не влезает, реплей ниже него, уровень
  k) + метка турна metadata.replayBelowConfiguredBudget=true — только когда реплей
  реально ниже настроенного бюджета (не на нормальном in-budget реплее).
- Тесты: перепиновка «не раздувает малый бюджет выше себя» (теперь МОЖЕТ резать ниже,
  Опция B) в обоих spec; новый тест эскалации ниже бюджета (окно 8000→бюджет 5600,
  k=1→2800, сходимость к полу, сброс на чистом ходу) + большое окно (140k→8k
  ступенями); тест метки метадаты. Мутации: пол=бюджет краснит below-budget тесты,
  снятие метки краснит observability-тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder force-pushed fix/520-recovery-escalation from 6f3237bc41 to 688cb54f26 2026-07-12 18:02:25 +03:00 Compare
agent_coder added review/needs and removed review/changes-requested labels 2026-07-12 18:02:27 +03:00
Collaborator

Ре-ревью — эскалирующая рекавери, Вариант B (#520). Вердикт: approved

Реализовано решение владельца (Вариант B) — рекавери МОЖЕТ резать реплей-бюджет ниже настроенного при повторных overflow. Проверил против кода + мутация.

📋 Проверка Option B

Формула (history-budget.ts:resolveEffectiveReplayThreshold) — ровно как решено:

const scaled = Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION ** k);
const floor = Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION));
return Math.max(scaled, floor);

Пол = min(8000, floor(threshold·0.5)) — абсолютный минимум, а не «=настроенному бюджету».

Поведение (проверил):

  • Малое окно (base=5600, chatContextWindow=8000): пол = min(8000, 2800) = 2800 → эффективный k0=5600 → k1=2800 (было запинено на 5600 = кирпич). Un-brick для малых окон достигнут.
  • Большое окно (base=100k): 100k→50k→…→8000-пол (как раньше). Не изменилось.

Тесты: новый «un-bricks: escalation drops below a small real window the fixed 0.5× never could» (history-budget.spec:50); ранее «enshrining»-спека «never inflates a small budget» переписана в «may cut below it, #520 Option B» (:70). Мутация-локк: вернул пол к старому min(FLOOR, threshold) (бриктующая версия) → 2 теста (un-brick + may-cut) КРАСНЕЮТ. Не вакуумно.

Гейт: unit 160/160 (было 156 + Option-B тесты), int (write-volume) 3/3 на Docker pg (6G heap). Own delta vs свежий develop = 6 файлов (recovery-скоуп; PR отребейзен на develop, ядро счётчика/математики было LGTM на эскалации). Дерево после мутаций чисто.

Эскалация закрыта: я вынес развилку → ты решил Вариант B → кодер реализовал по твоей/моей формуле → проверил + мутация-локк → approve. Готово к мержу.

## Ре-ревью — эскалирующая рекавери, Вариант B (#520). Вердикт: **approved** ✅ Реализовано решение владельца (Вариант B) — рекавери МОЖЕТ резать реплей-бюджет ниже настроенного при повторных overflow. Проверил против кода + мутация. <details> <summary>📋 Проверка Option B</summary> **Формула** (`history-budget.ts:resolveEffectiveReplayThreshold`) — ровно как решено: ```ts const scaled = Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION ** k); const floor = Math.min(REPLAY_MIN_FLOOR_TOKENS, Math.floor(thresholdTokens * REPLAY_AGGRESSIVE_FRACTION)); return Math.max(scaled, floor); ``` Пол = `min(8000, floor(threshold·0.5))` — абсолютный минимум, а не «=настроенному бюджету». **Поведение (проверил):** - Малое окно (base=5600, `chatContextWindow=8000`): пол = min(8000, 2800) = **2800** → эффективный k0=5600 → k1=**2800** (было запинено на 5600 = кирпич). **Un-brick для малых окон достигнут.** - Большое окно (base=100k): 100k→50k→…→**8000**-пол (как раньше). Не изменилось. **Тесты:** новый «un-bricks: escalation drops below a small real window the fixed 0.5× never could» (`history-budget.spec:50`); ранее «enshrining»-спека «never inflates a small budget» переписана в «may cut below it, #520 Option B» (`:70`). **Мутация-локк:** вернул пол к старому `min(FLOOR, threshold)` (бриктующая версия) → 2 теста (un-brick + may-cut) КРАСНЕЮТ. Не вакуумно. **Гейт:** unit **160/160** (было 156 + Option-B тесты), int (write-volume) **3/3** на Docker pg (6G heap). Own delta vs свежий develop = 6 файлов (recovery-скоуп; PR отребейзен на develop, ядро счётчика/математики было LGTM на эскалации). Дерево после мутаций чисто. </details> Эскалация закрыта: я вынес развилку → ты решил Вариант B → кодер реализовал по твоей/моей формуле → проверил + мутация-локк → approve. Готово к мержу.
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 18:14:51 +03:00
vvzvlad merged commit 4423b19850 into develop 2026-07-12 18:56:51 +03:00
Sign in to join this conversation.