Write down what three independent readings found about the spend ceiling: the shape is wrong rather than the rate, a paid call can vanish from the ledger on the happy path, and the fork is the owner's

This commit is contained in:
heaven 2026-09-04 21:16:01 +03:00
parent 1b8aa86cce
commit 0fafdd4bee

View file

@ -69,3 +69,119 @@
| Бэклог-строки | `docs/PROGRESS.md` строки 78 · 136 · 137 · 166 (135 и 165 закрыты D39.131) | | Бэклог-строки | `docs/PROGRESS.md` строки 78 · 136 · 137 · 166 (135 и 165 закрыты D39.131) |
| Контракт наружу | `14-api-contract/openapi.yaml` (`CeilingBounds`, `Usage`); ПТ-35 | | Контракт наружу | `14-api-contract/openapi.yaml` (`CeilingBounds`, `Usage`); ПТ-35 |
| Запрет денег на экране | D39.84; D39.100/К-8; ПТ-35 | | Запрет денег на экране | D39.84; D39.100/К-8; ПТ-35 |
---
## ⚠ ОТКРЫТАЯ РАЗВИЛКА 04.09: КНИГУ НЕЛЬЗЯ ДОЧИТАТЬ — разбор трёх независимых чтений, решение за владельцем
> **Статус: НЕ РАТИФИЦИРОВАНО.** Это разбор, а не контракт: ниже нет ни одного решения, только сходящаяся
> фактура и названная развилка. Носитель дефекта — `PD-440`/`PD-441` в регистре платформы. Собрано по
> прямому указанию владельца «посоветуйся с зонами и со старшей моделью»: читали независимо зона движка,
> зона платформы и внешний рецензент другой модели; расхождения между ними названы, а не сглажены.
**ЧТО НАБЛЮДАЛОСЬ.** Первый живой ПЛАТНЫЙ прогон книги через API (04.09) остановился, не продвинув книгу
ни на юнит, и воспроизвёлся дважды. Прогон умирает за 1015 секунд, списание не двигается.
**ПОЧЕМУ — цепь, проверенная тремя чтениями независимо.** Запас нового прогона на НОВЫЕ траты равен
`chaptersLeft × ставка` (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`; ставка
`DefaultPerChapter = 30_000` микро-USD). Резерв ОДНОГО редакторского вызова — worst case по `maxTokens`
(`backend/internal/pipeline/stagerun.go`, греп `func (r *Runner) callEstimateUSD`), замеренный отказ в
ряду `PD-440``denied estimate=$0.069828`. Отказ резервации трактуется как **авария всей книги**:
`runWave` зовёт `cancel()` производного контекста на ПЕРВОЙ ошибке любого рода, и уже допущенные
параллельные вызовы гасятся.
**ТРИ УТВЕРЖДЕНИЯ, СОШЕДШИЕСЯ У ВСЕХ ТРЁХ ЧТЕНИЙ:**
1. **Форма потолка неверна, а не только его размер.** Запас сжимается вместе с остатком книги, стоимость
неделимого шага — нет. Ставку можно поднять, стену это отодвинет, но не уберёт. Правильная форма —
`max(главы × ставка, шаг)`, а не `главы × ставка`.
2. **Резервацию убирать нельзя.** Она — единственный сериализованный контроль и единственное, что держит
обещание «не потратить больше, чем дано». Отраслевой стандарт тот же (см. прайор-арт ниже).
3. **Менять надо ТРАКТОВКУ ОТКАЗА**, а не наличие резервации: «не начинаем новое» вместо «гасим всё».
**ЧИСЛА, В КОТОРЫХ ЧТЕНИЯ РАСХОДЯТСЯ — и оба верны на своём числе воркеров.** Зона платформы: стена
стоит везде, где `chaptersLeft × ставка < workers × шаг`, при боевых четырёх воркерах это
`chaptersLeft ≤ 9` — недостижим не хвост, а **последние девять глав любой книги**. Внешний рецензент:
даже при ОДНОМ воркере `2 × $0.03 = $0.06 < $0.07` одного вызова, то есть **последние две главы
недостижимы в принципе**, независимо от параллельности. Второе — инвариантный пол, первое — сегодняшняя
конфигурация.
**Предсказание зоны платформы, ЗАМЕР ЗАКАЗАН, на момент записи НЕ подтверждён:** книга из ≤9 глав может
не переводиться вообще никогда. Проверяется за $0 на пятиглавой книге. До подтверждения — предсказание.
**ЧЕТВЁРТОЕ, ЧЕГО НЕ БЫЛО В ИСХОДНОМ ДИАГНОЗЕ (зона движка).** Резерв завышен ПО ПОСТРОЕНИЮ: оценка
считается по потолку выдачи. Под нынешним поведением он никогда не превращается в факт — его отпускают,
и следующее решение о допуске принимается против тех же завышенных чисел. Под «не начинаем новое»
соседи доседают по ФАКТИЧЕСКОЙ цене и освобождают разницу. Это само по себе может быть разницей между
«встала» и «дошла».
**ПЯТОЕ — ПРЕЦЕДЕНТ ВНУТРИ ЭТОГО ЖЕ ПАКЕТА, и он решает спор о направлении.** Объёмный потолок уже
построен с нужной семантикой и САМ противопоставляет себя денежному:
`backend/internal/pipeline/volume.go`, греп `THE STOP IS A COMPLETION, NOT A HALT` — «денежный потолок
останавливает прогон ПОСРЕДИ работы, которую он собирался сделать». Там же названа причина, по которой
допуск не сделан счётчиком у воркеров: «воркер, которому отказали в слоте, был бы вынужден бросить свой
элемент навсегда» — то есть ровно тот дефект, который денежный путь и имеет. Плюс три места уже
деградируют по `errReserveCeiling` вместо аборта (`escalation.go`, `repair.go`, `terminologist.go`);
не деградирует только главная волна.
**ШЕСТОЕ — ДЕФЕКТ, ПЕРЕЖИВАЮЩИЙ ЛЮБУЮ ПОЧИНКУ РАЗМЕРА И ФОРМЫ (зона платформы).** Стена МОЛЧАЛИВА:
прогон, вставший мгновенно, неотличим на всех поверхностях от работавшего — одинаковый статус, одинаковая
причина паузы, одинаковая полоса (наблюдение H16 зонного журнала, два прогона: $0.081 и $0). Недобрать
можно при любом потолке; сказать об этом сегодня нечем.
**ВЕС — ЖИВУЧЕСТЬ, НЕ ДЕНЬГИ.** Мёртвые прогоны рассчитались в ноль, леджер сходится тремя источниками
до микродоллара. Пользователь теряет ВРЕМЯ. ⚠ Отдельно и с другим весом — `PD-441`: на пути отмены есть
расход, который, ЕСЛИ он был, невидим для леджера по построению. Доказуемая форма именно такая; «деньги
потрачены» НЕ измерено. Одна движковая попытка — до трёх запросов к провайдеру (`backend/internal/llm/httpllm.go`,
греп `for att := 0; att < profile.MaxAttempts`, дефолт 3) под ОДНИМ резервом и ОДНОЙ строкой `request_log`:
след не просто беден, он агрегирован и не различает «не ушло ничего» от «ушло трижды».
**ПРАЙОР-АРТ (внешний рецензент, ссылки — в отчёте сессии).** LiteLLM делает буквально наше:
worst-case-резерв → отказ до отправки → замена фактической ценой; их известные баги — про ОБХОД бюджета
при параллельных запросах, то есть отраслевая беда противоположна нашей. Ближайший по смыслу стандарт —
Diameter Credit-Control (RFC 4006/8506) с `Final-Unit-Indication`: «это последний грант, доработай его и
не начинай нового» — ровно та семантика, которой нам не хватает. Kubernetes `ResourceQuota`: отказ — это
недопуск НОВОГО, уже созданное квота не трогает. Slurm `GraceTime`: слив вместо убийства. Карточная
пре-авторизация: холд с запасом, списание по факту. ⚠ Про биллинг при обрыве: OpenRouter документирует,
что для непотоковых запросов отмена клиентом биллинг НЕ останавливает — это факт о ЕГО платформе, для
наших провайдеров вопрос обязан быть закрыт вендор-докой по правилу `experiments/00-provider-quirks.md`,
а не рассуждением.
**РАЗВИЛКА ВЛАДЕЛЬЦА — ОДНА, И ЕГО ПРЕДЛОЖЕНИЕ ЕЁ РЕШАЕТ, А НЕ ОБХОДИТ.** Предложение владельца
(«называть примерную цену и останавливаться, когда деньги кончились») означает, что потолком становится
БАЛАНС. Тогда холд берёт весь баланс вперёд, и вторая книга не стартует, пока не рассчитается первая.
Это ровно та развилка, которую код уже держит открытой и помечает вопросом к владельцу:
`platform/internal/pricing/pricing.go`, греп `which fraction is a product question`. ⚠ Отдельный выбор
того же разговора: платят вызовом за ЮНИТ, а человек видит и выбирает ГЛАВЫ, и ставка за главу сама
признаёт слабость (греп `chapters differ in length`); `units_total` известен платформе до всякой оплаты.
**ЧТО СЧИТАЕТСЯ ЗАКРЫТЫМ И БОЛЬШЕ НЕ ОБСУЖДАЕТСЯ:** уже проданные прогоны смену ставки переживают без
миграции — продолжение оценивается по тому, за что прогон был ПРОДАН, бюджет читается обратно из холда
первой попытки, пин `TestARestartHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince`.
**СЕДЬМОЕ — ЕДИНИЦА УЧЁТА НЕ СОВПАДАЕТ С ЕДИНИЦЕЙ БИЛЛИНГА, и это бьёт не в отчётность, а в ЦЕЛОСТНОСТЬ
потолка** (найдено зоной движка чтением под вопрос, проверено оркестратором по строкам). Класс
`BilledDecodeError` заведён ровно для того, чтобы оплаченный-но-нечитаемый вызов стал ВИДЕН: рантайм
консервативно доседает его по оценке. Комментарий рядом сам называет цену ретрая —
`backend/internal/llm/httpllm.go`, греп `each retry is a new billed`, — и по этой причине УСЕЧЁННЫЙ ответ
сделан терминальным. Но битое тело осталось ретраябельным, а `retryLoop` при успешном ретрае возвращает
ответ и предыдущую ошибку ВЫБРАСЫВАЕТ (греп `if err == nil {` в `retryLoop`). Тогда рантайм видит один
успешный ответ, доседает его `Usage`, и оплаченный 2xx перед ним не попадает НИКУДА — ни в списание, ни
отдельной строкой лога. **Механизм, построенный чтобы сделать оплаченный вызов видимым, обходится ровно
на том пути, где всё прошло хорошо.** Следствие для потолка: допуск выдан один раз под одну оценку; если
под ней прошло два оплаченных ответа, `Reserve` об этом не узнает никогда — он сравнивает с потолком
сумму, в которой невидимого запроса нет. Потолок не «пробивается громко», он считает не то.
**Границы:** структурность доказана чтением; ЧАСТОТА пути НЕ измерена и, вероятно, мала — владельцу
эти две вещи давать раздельно. Более слабый родственник — сетевой обрыв/таймаут (там же комментарий про
`mid-generation RST that re-bills the call`): вход тот же, но «оборвалось на середине ⇒ оплачено» уже
вывод о провайдере, а не утверждение кода.
**ВЕНДОРСКИЙ ВОПРОС — ДВОЙНОЙ, и вторая половина конкретнее первой:** (1) платит ли провайдер за запрос,
оборванный КЛИЕНТОМ после начала генерации; (2) сколько раз он берёт деньги, когда мы ретраим ПОСЛЕ
оплаченного 2xx. Второй случай не гипотетический — он уже назван оплаченным в нашем собственном коде.
Оба закрываются вендор-докой по правилу `experiments/00-provider-quirks.md`, а не рассуждением.
**ГРАНИЦА ИНСТРУМЕНТА, ЗАМЕРЕННАЯ ЭТИМ ЖЕ ЗАХОДОМ.** Каталог мутаций (141 запись, 136 красных, ноль
переживших) доказывает ПОВЕДЕНИЕ и молчит о том, что код УТВЕРЖДАЕТ о себе. Круг 3 нашёл четыре
сверх-утверждения — и **ни одно не нашла посадка**; обе сегодняшние денежные находки получены чтением под
вопрос. Рабочая форма вопроса, выведенная зоной движка: не «правда ли то, что здесь написано», а **«что
должно было бы сломаться, если это правда, и ломается ли оно»**.