diff --git a/docs/architecture/15-money-path.md b/docs/architecture/15-money-path.md index 5222c865..d8a04127 100644 --- a/docs/architecture/15-money-path.md +++ b/docs/architecture/15-money-path.md @@ -69,3 +69,119 @@ | Бэклог-строки | `docs/PROGRESS.md` строки 78 · 136 · 137 · 166 (135 и 165 закрыты D39.131) | | Контракт наружу | `14-api-contract/openapi.yaml` (`CeilingBounds`, `Usage`); ПТ-35 | | Запрет денег на экране | D39.84; D39.100/К-8; ПТ-35 | + +--- + +## ⚠ ОТКРЫТАЯ РАЗВИЛКА 04.09: КНИГУ НЕЛЬЗЯ ДОЧИТАТЬ — разбор трёх независимых чтений, решение за владельцем + +> **Статус: НЕ РАТИФИЦИРОВАНО.** Это разбор, а не контракт: ниже нет ни одного решения, только сходящаяся +> фактура и названная развилка. Носитель дефекта — `PD-440`/`PD-441` в регистре платформы. Собрано по +> прямому указанию владельца «посоветуйся с зонами и со старшей моделью»: читали независимо зона движка, +> зона платформы и внешний рецензент другой модели; расхождения между ними названы, а не сглажены. + +**ЧТО НАБЛЮДАЛОСЬ.** Первый живой ПЛАТНЫЙ прогон книги через API (04.09) остановился, не продвинув книгу +ни на юнит, и воспроизвёлся дважды. Прогон умирает за 10–15 секунд, списание не двигается. + +**ПОЧЕМУ — цепь, проверенная тремя чтениями независимо.** Запас нового прогона на НОВЫЕ траты равен +`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 нашёл четыре +сверх-утверждения — и **ни одно не нашла посадка**; обе сегодняшние денежные находки получены чтением под +вопрос. Рабочая форма вопроса, выведенная зоной движка: не «правда ли то, что здесь написано», а **«что +должно было бы сломаться, если это правда, и ломается ли оно»**.