Hand the engine the calls it cuts itself: the money boundary is delivery of the request, not the status line

This commit is contained in:
heaven 2026-09-08 17:48:14 +03:00
parent bf12bfb0cf
commit 60d575164d
3 changed files with 347 additions and 1 deletions

View file

@ -0,0 +1,344 @@
# Пак бэкенда: «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ»
## 1. Какая проблема и что решит твой результат
Движок отправляет запрос провайдеру, тот принимает его и генерирует — а мы по своему же дедлайну
перестаём ждать и рвём соединение. Провайдер об этом не знает и работу не отменяет. Дальше движок
**выбрасывает единственную улику**: `internal/llm/httpllm.go:409``data, _ := io.ReadAll(...)`, ошибка
чтения уходит в `_`, а обрыв распознаётся по РАЗМЕРУ. Тело, оборванное нашим дедлайном, маленькое ⇒ не
отличимо от целого ⇒ не разбирается как JSON ⇒ объявляется «оплаченным 2xx с нечитаемым телом» и
**ретраится** (`:433` терминален только для >16 МиБ; `:365` даёт один повтор).
**Цена — один замер и одно наблюдение, и разница между ними важна.** `docs/experiments/23-editor-tier.md` §5: латентности ровно `2×attempt_s`,
**$0.475893 = 23.8 %** цены руки — это САМИ четыре строки; юниты целиком, вместе с их попытками 0, —
**$0.689215 = 34.5 %**. ⛔ Источник пишет дословно «две законные цифры, и путать их нельзя» и регистрирует
эту путаницу СВОИМ дефектом №11; не повторяй её, называя одно число двумя долями. Живой прогон 08.09: два редакторских вызова,
`unexpected end of JSON input`, **$0.407606 из $1.259217 = 32 % бюджета**. ⚠ **Что это ИМЕННО таймауты —
не измерено, а выведено по совпадению подписи** с замером выше: сырого леджера того прогона в дереве нет
(`.db` новее 07.09 — 0 при 163). Не опирайся на второе число как на улику класса.
**Корень — НЕ транспорт, а дедлайн против оси регенерации.** DeepSeek держит 7576 tok/s (n=139). Грант
редактора 16 000 ≈ 213 с; реген удваивает его до 32 000 ≈ **427 с** и НЕ трогает время, а `attempt_s`
равен 240. Потолок по времени 1721 тыс. токенов; максимум, когда-либо доставленный регеном, — **19 354 =
60 % гранта**, и **13 доставок из 17 уложились бы в исходные 16 000**. ⇒ ось эскалации сегодня фиктивна:
резервация бронирует недостижимое и съедает headroom под книжным потолком (строка **369**).
**Что покупает твой результат.** (1) Мы перестаём покупать одну генерацию дважды. (2) Оператор и читатель
видят, какие деньги — оценка, а не факт. (3) Эскалация становится настоящей: правильный дедлайн делает
выданный бюджет достижимым.
## 2. Зона и git
**Зона записи — `backend/`** плюс своя секция в `docs/PROGRESS.md` (это ЕДИНСТВЕННОЕ исключение из
«`docs/` — зона оркестратора»). Чужие зоны (`platform/`, `frontend/`, `eval/`, `books/`) не трогаешь ни
строкой; расхождения — пингом. **Ты НЕ коммитишь**: дерево готовишь и передаёшь на лендинг. Никаких
`git add -A`, `reset --hard`, переписывания истории. `.env` не читать никогда.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `docs/BACKLOG.md` — строки **360** (несёт СОШЕДШУЮСЯ конструкцию лечения целиком), **369** (ось
эскалации), **78** (движковая половина `PD-441`: публикация оценочных строк). Греп `^| 360 |`.
2. `docs/experiments/00-provider-quirks.md` — **канон велит читать ПЕРЕД правкой адаптеров и вызовов.**
⚠ Класса, который ты чинишь, там НЕТ (замер: `early-200`/«empty lines»/«10 minutes» — 0 хитов) — это
пробел, и он пингом полигону, не твоя правка.
3. `docs/research/21-llm-transport-survey.md` — §Q6 и **ЭРРАТА ОРКЕСТРАТОРА 08.09 в шапке**: обзор неверен
дважды, и обе половины вместе полтора месяца держали неверный дедлайн. Читай эррату ПЕРЕД §Q6.
4. `docs/architecture/15-money-path.md` — денежный путь; леджер есть НИЖНЯЯ граница.
5. `backend/cmd/tmmutate/main.go` — шапка целиком (контракт мутационного инструмента) + `backend/Makefile`,
цели `battery` и `mutations`.
Ратифицированное, нужное по существу, вложено сюда и похода в журнал решений НЕ требует:
- ⛔ **`D39.230` п.1 — СЛОВО ВЛАДЕЛЬЦА, и у него есть УСЛОВИЕ.** Оценка за оборванный нами вызов
списывается с читателя. **Но пометка — часть решения, а не украшение:** строка обязана нести свой класс
и флаг `estimated=1`, а счёт и сумма оценочных строк — публиковаться машинно-читаемо рядом с
`committed_usd`. Списать с человека непроверенную оценку без видимой пометки — НЕ то, на что дано слово.
- **`D39.230` п.2** — платная проба вендорского баланса ОТКЛОНЕНА владельцем. ⇒ вопрос «биллит ли вендор
оборванное» осознанно ОТКРЫТ; оценка есть верхняя граница под гипотезой «вендор досчитывает». Проб не
проектировать и не предлагать.
- **`D39.230` п.3** — ожидание провайдера до ~20 минут РАЗРЕШЕНО. Сверху не убьёт ничто: `RuntimeMaxSec`/
`WatchdogSec` — 0 хитов при 16 файлах с systemd-свойствами, у движка дедлайна нет.
- **`D39.216`** — где форма не тянет, её развязывают или переписывают; «дешевле подпереть» доводом НЕ
является; второй носитель одного знания не заводится (п.3б).
- **`D39.183`** — запрет править тесты «чтобы прошло» про МОТИВ; правка от ЗАКАЗАННОЙ смены поведения —
обслуживание, но объявляется отдельным пунктом отчёта.
- **`D39.202`** — прибор, честно отвечающий на СВОЙ вопрос, читается как ответ на другой. Ноль и «не
смотрели» неразличимы без контрольной величины.
## 4. Состав и разметка свободы
### 4.1 Признак: граница денег — ДОСТАВКА запроса, а не статус-строка — **делай РОВНО так**
⛔ **Доктрина «2xx ⇒ провайдер уже списал» (`httpllm.go:428`, `stagerun.go:610`) ЛОЖНА для нашего боевого
провайдера.** DeepSeek документирует ранний `200` с пустыми строками, пока запрос ждёт планирования, и
закрытие соединения, если инференс не начался за 10 минут; в наших логах пустое тело при `200` видно буквально
(`response="\n"`, **8 строк из 617** «llm exchange» под `books/gu-zhenren/` — счёт мой). ⚠ **Но НЕ бери эту строку как
улику self-cut: она пришла на 91-й секунде при дедлайне 240 — ушли не мы.** Настоящие self-cut в том же
файле лежат ровно на **240.0 с** — четыре трейса в `probe-4axes/logs/p7-off.log` ⚠ (счёт опровергателя
приёмки, оркестратором НЕ пере-снят; пере-сними сама, если будешь на них опираться). Разница и есть предмет
пака: одна подпись, два разных класса, и различает их только признак §4.1. ⇒ `200` значит «принят», не «сгенерирован», и ветвь `No 2xx ever arrived`
на DeepSeek недостижима по построению — все наши обрывы прятались под `decode_error`.
**Единственный факт, общий для всех этих случаев: запрос ДОСТАВЛЕН, и ушли МЫ.** Он фиксируется без байта
на проводе — `httptrace.ClientTrace{WroteRequest, GotFirstResponseByte}` в `attempt()`. Различение
проверено исполнением на клиенте той же сборки:
| случай | `WroteRequest` | `GotFirstResponseByte` |
|---|---|---|
| доставлен, заголовки держат, наш дедлайн | `true` | `false` |
| TLS-рукопожатие висит — НЕ доставлен | `false` | `false` |
| ранний `200` + `"\n"`, наш дедлайн | `true` | `true` |
**Заказ:** (а) ошибка `ReadAll` больше не выбрасывается; (б) заводится типизированная
`AttemptTimeoutError{Delivered, AfterHeaders, BytesRead, WhitespaceOnly, Elapsed}`; (в) исходы —
**перечислены ниже поимённо, свой набор не собирай**. ⛔ Обёртывание обязано СОХРАНИТЬ
`errors.Is(err, context.Canceled)` для кодов выхода — иначе стоп прогона получит чужой exit-код.
**ПОСЛЕ 2xx — пять исходов:**
| исход | признак | вызовов | деньги | чанк |
|---|---|---|---|---|
| целое, JSON ок | `readErr == nil` | 1 | usage | как есть |
| целое, JSON битый | `readErr == nil`, разбор упал | **2** (кап 1, как сегодня) | оценка | флаг |
| **self-cut** | `readErr != nil`, дедлайн `attemptCtx`, родитель ЖИВ | 1 | `settleUSDForCutCall` | флаг; резюм — **0** вызовов |
| **отмена РОДИТЕЛЕМ** (стоп, Ctrl-C) | `readErr != nil`, `ctx.Err() != nil` | 1 | `settleUSDForCutCall` | ⛔ своя причина `cancelled`, НЕ общий флаг «нужен человек»; резюм — 1 вызов на ТОМ ЖЕ бюджете |
| разрыв соединения | `readErr != nil` иначе (`ErrUnexpectedEOF`, h2 stream) | 2 | `settleUSDForCutCall` | на исчерпании — **инфра-пауза, НЕ флаг** |
**ДО заголовков — четыре:** self-cut доставленный (1 вызов, флаг, резюм 0) · отмена родителем
доставленного (1, деньги те же, резюм 1 на том же бюджете) · разрыв после доставки (2, деньги те же,
инфра-пауза, резюм 1) · **НЕ доставлен** (до `max_attempts`, **$0**, освобождение резерва, резюм 1 — это
первая покупка, а не перекупка).
**`TestBilledDecodeErrorReBillCap` не меняется** — у него `readErr == nil`, это второй исход таблицы.
### 4.2 Диспозиция: не ретраить то, что оборвали мы — **делай РОВНО так**
- **Доставлен + наш дедлайн** (с заголовками и без) — класс `attempt_timeout`, **чекпойнт + флаг, БЕЗ
ретрая**, сумма — через `settleUSDForCutCall`.
- **НЕ доставлен** (дедлайн до `WroteRequest`) — единственный случай, где «ничего не куплено» верно по
построению: остаётся как сегодня (retryable, на исчерпании освобождение резерва, инфра-пауза).
- ⛔ **ОТМЕНА РОДИТЕЛЕМ доставленного вызова** (стоп прогона, Ctrl-C) — **это тот самый случай, с которого
начался заказ (строка 331), и диспозиция у него ДРУГАЯ.** Деньги — та же `settleUSDForCutCall` (ровно то, что просит
`PD-441` — по-русски: «сеттлить оценку и при отмене вызова, который успел уйти, различая по ФАКТУ ухода»), но чанк **НЕ флагуется**: вызов был здоров, остановил его ЧЕЛОВЕК. ⛔ **И резюм обязан пере-делать
его на ТОМ ЖЕ `max_tokens`** — `maxTokensForAttempt` удваивает бюджет по оси попыток
(`disposition.go:393-401`, `base << uint(attempt)`; петля берёт его из НОМЕРА попытки,
`stagerun.go:147`), и здесь это ловушка: удвоение купит вдвое больший вызов за здоровье, которого никто
не терял.
⛔⛔ **КАК это построить — задано, потому что наивный путь НЕВОЗМОЖЕН, и это проверено.** В сторе ровно
три функции (`Reserve`, `Release`, `SettleWithCheckpoint`), деньги нельзя записать БЕЗ чекпойнта, а
чекпойнт — ключ резюма (`ON CONFLICT (request_hash) DO NOTHING` ⇒ «Duplicate settle: book no new
spend»). ⇒ «заплатить и пере-сделать на том же ключе» несовместимо по построению.
**Развязка — в ПЕТЛЕ, а не в сторе:** индекс попытки и счётчик УДВОЕНИЙ разводятся.
`maxTokens = base << escalations`, где `escalations` растёт только на `length`/`empty`; кап регенерации
считает УДВОЕНИЯ, а не индекс. Тогда попытка N+1 берёт тот же бюджет, но НОВЫЙ хеш (`Attempt`
самостоятельное измерение) ⇒ свежие `reserve → call → settle`, `ON CONFLICT` недостижим, инвариант
`committed == sum(checkpoints)` цел, стор не тронут. ⛔ **Стор и его инварианты не трогать** — упрёшься,
это пинг.
⚠ И проверь побочное: `priceprojection.go` считает проекцию через `maxTokensForAttempt` («RETRIES ARE IN
IT, AND THAT IS THE POINT») — смена оси двигает СМЕТУ. Двинулась — назови в отчёте числом. ⚠ **Видимое следствие, назови его в отчёте:** стоп прогона
теперь ПЛАТИТ оценку за каждый летящий вызов (в редакторской волне это до восьми × ~$0.13). Слово
владельца это покрывает («оборванный НАМИ» — мы рвём по его команде), но молча вводить это нельзя.
- **Разрыв соединения ПОСЛЕ доставки** — один ретрай (кап ключуется на «доставлен», а не на «2xx»), на
исчерпании **инфра-пауза, не флаг**: серия таких — мёртвый провайдер, и довод нулевой ветви про
флаг-шторм здесь верен. ⛔ **Но деньги назвать обязательно, иначе граница течёт через эту дверь:**
сегодняшний инфра-путь зовёт `releaseReservation` и даёт **$0**, а запрос был ДОСТАВЛЕН ⇒ идёт
`settleUSDForCutCall` + класс `connection_lost`, и ТОЛЬКО потом пауза; резюм пере-делает на том же
бюджете. Для post-header разрыва это сегодняшнее число, для pre-header — новое, и объявляется
заказанной сменой поведения.
⚠ Довод против «оставить ретрай, он же 3 раза из 7 спасал»: правильный дедлайн даёт то же самое без
монетки по цене полного счёта. Клапан на будущее — ДАННЫЕ (`tok_s_floor`), не код.
### 4.3 ⛔ ТРЕТЬЯ УТЕЧКА, которой нет даже в строке 331 — **делай РОВНО так**
Сегодняшняя нулевая ветвь (`stagerun.go:634`) зовёт `releaseReservation` и **чекпойнта НЕ пишет**, ставя
стадию `failed`**резюм ПОКУПАЕТ ВЫЗОВ ЗАНОВО**. Убрать ретрай и оставить это значит закрыть класс
наполовину: тот же платный вызов уходит другой дверью.
**Заказ:** доставленный self-cut получает чекпойнт, и резюм его находит. **Предъявление именно этим:** тест
утверждает, что после self-cut резюм на том же снапшоте **не делает НИ ОДНОГО HTTP-вызова**. Мутация:
убрать запись чекпойнта — тест обязан покраснеть текстом про свежий вызов, а не про деньги.
### 4.4 Дедлайн считается от бюджета — **делай РОВНО так по форме, числа реши сам и обоснуй**
`timeouts` получает `tok_s_floor`, `queue_slack_s`, `attempt_max_s`; дедлайн вызова =
`clamp(queue_slack_s + max_tokens/tok_s_floor, attempt_s, attempt_max_s)`, считается в `attempt()` из
`maxTokens` запроса. **`attempt_s` становится ПОЛОМ** ⇒ ни один существующий конфиг не теряет времени.
Дефолт пола — **35.5 tok/s** (128 000/час, единственное опубликованное вендором число).
**Почему этот дефолт правильный, и это же твой тест:** он ПРОИЗВОДНЫЙ, и на черновике (8 496 токенов)
даёт **238.95 с** — сегодняшние 240 с точностью до секунды. То есть нынешнее значение верно ровно для
черновика и было перенесено на редактора слепо. ⛔ **Тест-тождество обязан пинить ФОРМУЛУ, а не число, и вот операционная форма — иначе его исполнят
формально.** `assert derive(8496) == 238.95` негоден: это литерал, произведённый формулой, и через полгода
его «починят» под круглые 240. Требуется: ожидание **вычисляется в тесте** из двух вендорских констант
(час, 128 000) и гранта; **литералов `238`/`239`/`240`/`900` в тест-файле НЕТ** — предъяви грепом с
контрольной величиной; не меньше ДВУХ грантов (8 496 и 32 000); отношение удвоения
`derive(2N) slack == 2·(derive(N) slack)`; края `clamp`; пустое и нулевое поле дают ДЕФОЛТ, а не
деление на ноль и не нулевой дедлайн.
**Твоё — ЧИСЛА. Моё — ПРАВИЛО их вывода, и оно РОВНО так:**
- `tok_s_floor` — НИЖЕ p10 наблюдаемой скорости модели по ≥100 строкам `request_log`, округление вниз
объявлено (DeepSeek: p10 ≈ 67 ⇒ 50);
- `queue_slack_s` — документированное ВЕНДОРОМ максимальное ожидание до инференса (DeepSeek — 600).
⛔ Меньше — только с доводом и ПИНГОМ: владелец сказал «готов ждать» именно про ~20 минут, и меньший слак
меняет принятый им размен молча, в другую сторону;
- `attempt_max_s` — слак + максимальный грант С УДВОЕНИЕМ / пол, округлённый вверх (DeepSeek:
600 + 32000/50 = 1240).
**И два механизма, без которых «новый провайдер без правки Go» не работает:** WARN на старте для
провайдера БЕЗ `tok_s_floor`, чей стадийный грант с учётом удвоения выше ~8.5k («дефолт 35.5, дедлайн для
32 000 = 900 с, задай после первого прогона»), и валидационный тест `models.yaml`, ТРЕБУЮЩИЙ поле для таких
провайдеров.
**Снапшот не двигается, и проверено это ПРЯМО, а не соседством:** `buildSnapshotID`
(`internal/pipeline/snapshot.go`) перечисляет входы поимённо, и таймаутов среди них нет — читай сам список,
а не грепай близость слов. ⛔ Прежняя редакция этого пака писала «`AttemptS` — 0 хитов из 120 файлов со
`snapshot`»: замер был неверен в ЧИСЛИТЕЛЕ (хит 1 — `config/models.go`) и в ЗНАМЕНАТЕЛЕ (файлов 145), а
вывод верен — правый вердикт по неправой причине, `D39.202`. Если твоя правка список входов меняет — СТОП
и пинг: перекупка книг словом владельца не покрыта.
### 4.5 Пометка — УСЛОВИЕ решения владельца — **делай РОВНО так**
Класс `attempt_timeout` и `estimated=1` едут в чекпойнт и `request_log`; `classify()` узнаёт класс на
резюме; `paidtail` включает его в LostUSD. **И сверх того — то, ради чего дано слово:** счёт и сумма
оценочных строк публикуются **машинно-читаемо рядом с `committed_usd`** (`D39.230` п.1; это же движковая
половина `PD-441`, строка **78**). ⚠ **Поверхность назови точно: это тот JSON, который ЧИТАЕТ платформа
(`tmctl status --json`), а не новый файл.** Формат — пингом платформе, не решением за неё. ⚠ Сегодня флаг `estimated` виден только человеку в `tmctl render.go`;
платформа его не читает — ей нужно число, а не строка экрана.
### 4.6 Оператор не должен принимать ожидание за висяк — **реши сам, бери если по пути**
С разрешённым ожиданием до ~20 минут INFO-лог говорит «calling model» и потом молчит. Дешёвая строка «жду
провайдера: прошло X из Y» раз в N минут снимает это. ⚠ И назови в отчёте, что `waitForSettle` теперь ждёт
до самого длинного летящего вызова — это принятая цена решения владельца, но она должна быть названной.
### 4.6-бис Мандат чистоты — **делай РОВНО так**
Пак ПРОЕКТИРУЕТ: новый тип ошибки, три новых поля конфига, новая денежная семантика. ⇒ `D39.216` действует
буквой: где нынешняя форма не выдерживает заказ, её **развязывают или переписывают**, а не обкладывают
заплатой; «дешевле подпереть» доводом НЕ является; второй носитель одного знания не заводится (п.3б).
Отступление законно, но объявляется С ЦЕНОЙ: почему заплата дешевле развязки и что именно она откладывает.
⚠ Нота требует этой строки в КАЖДОМ паке с проектированием и называет её отсутствие дефектом промта — она
здесь не для полноты.
### 4.7 Чего в паке НЕТ**не делай**, и это ОБЪЯВЛЕННЫЕ сужения
- **Стриминг** — не спас бы ни одно из 19 замеренных событий; цена: `stream:true` в теле ⇒ `RequestHash`
перекупка книги, непроверенная байт-идентичность склейки, новый парсер, потеря usage-fidelity. Trip-wire
обзора переписан: доля self-cut в спенде редактора ПОСЛЕ починки дедлайна.
- **Платная проба вендорского баланса** — ОТКЛОНЕНА владельцем (`D39.230` п.2). Не проектировать.
- **Адаптивный пол из леджера на лету** — прячет число от оператора, тащит состояние прогона в дедлайн.
- **`max_attempts` и бэкофф** — другая ось, не трогать.
- **Порог по латентности** («≈2×attempt_s») — совпадение; протухнет молча при смене `attempt_s`.
- **Idle-timeout в non-streaming** — схлопывается в общий бюджет (§Q6 прав).
- **Разбор обрывков тела.**
- **Оценка по времени `elapsed × rate`** — догадка на догадке; оправдана только исходом пробы, которой не будет.
- **Денежная сумма как YAML-ручка** — политику нельзя переключать молча конфигом; она за одной функцией.
- **Строка 368 (`est_tokens` занижена в 5 раз)** — отдельный носитель, другой предмет. Наткнёшься —
наблюдением в отчёт, не чини: два денежных класса в одном лендинге мы сегодня уже разводили.
- **Anthropic-адаптер** (`provider_anthropic.go`) — ВНЕ пака целиком: поверхность deprecated (из стека
убрана 04.07), боевой цены ноль, а тесты и мутации она бы добавила. Уходит строкой бэклога.
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается
1. **Каждая гарантия предъявляется ИСПОЛНЕНИЕМ на фейк-сервере**, а не чтением. Пять исходов после 2xx и
четыре до заголовков — **ДЕВЯТЬ фикстур**, и у каждой утверждается ЧИСЛО HTTP-вызовов, а не только исход.
**Счёт вызовов берётся у ФЕЙК-СЕРВЕРА** (сколько запросов он принял), а не счётчиком клиента: клиент
считает свои намерения, сервер — факты.
⛔⛔ **И к КАЖДОЙ доставленной-и-потерянной фикстуре — ПАРНОЕ утверждение о РЕЗЮМЕ**, иначе конструкция
вырождается в противоположную сторону: сессия может «пере-закрыть» — не пере-делывать НИЧЕГО
доставленного — и остаться зелёной. Ожидания: self-cut → резюм **0** вызовов; отмена родителем и
исчерпанный разрыв → **ровно 1 вызов на ТОМ ЖЕ `max_tokens`**; не доставлен → 1.
**Поля утверждаются ДВУСТОРОННЕ, иначе они декоративны:** `WhitespaceOnly``true` на теле `"\n"` и
`false` на обрывке JSON; `Delivered`обе стороны. И денежное утверждение на каждой строке:
`estimated=1`, `cost_usd == estimate`, класс на месте; у «не доставлен» — `reserved_usd` вернулся в 0.
2. **Мутации — на копии, рецепт УЖЕ в цели `mutations`** (`backend/Makefile`); своего однострочника не пиши.
Краснота читается ПО ТЕКСТУ падения: говорит ли он про сломанное тобой.
3. **Мутируй и СВОИ новые фикстуры.** ⛔ Особая осторожность: у тебя условные сообщения (пометка
`estimated`, WARN про пол провайдера) — на каждое нужна фикстура, где оно ОБЯЗАНО прозвучать, И вторая,
где обязано молчать. Тест, утверждающий только молчание, вакуумен.
4. **Субагенты РАЗРЕШЕНЫ ЯВНО** — и на охват, и на адверсариальный проход по ГОТОВОЙ работе.
**Направление — что уязвимо ИМЕННО в этом паке** (норма куплена замером 08.09: направленный проход
нашёл шесть дефектов пака на ЗЕЛЁНОЙ батарее): **(1)** фикстуры, где «доставлен» и «не доставлен»
случайно совпали — весь пак стоит на этом различении; **(2)** путь резюма: чекпойнт написан, но
`classify()` его не узнаёт ⇒ перекупка возвращается тихо; **(3)** `errors.Is(context.Canceled)` после
твоего обёртывания — коды выхода ломаются молча, И одновременно `errors.As` обязан НАХОДИТЬ твой тип:
два утверждения в одной фикстуре, не в разных; **(4)** исход «отмена родителем» — его легко потерять,
потому что он единственный, где деньги списываются, а флага НЕТ.
5. **Проверяемый артефакт** — таблица «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО» плюс столбец
**«закрыт в N носителях из M»**, где M посчитан командой.
6. **BASELINE НА ВХОДЕ сними САМ** и сверь с моим: `make battery` → 19 `ok` · 0 FAIL · 4 «no test files» ·
4 скипа; `make mutations`**65 записей · 0 выживших · свип `0 of 276`**; `python3
docs/scripts/counts.py --check` → «Литералы сходятся». ⛔ **Печатай счёт СКИПОВ рядом с числом `ok`** —
норма куплена 08.09: «20 пакетов ok» и «20 ok при 431 скипе» неразличимы, и приёмка так приняла механизм,
ни разу его не коснувшись. Красное на входе — находка и пинг, а не твоя работа.
## 6. Оси ревью — характер «Код», плюс деньги и шов (ось вправе заменить с аргументом)
Тест утверждает не то, что думает (`D39.171`): часы как способ синхронизации там, где предмет НЕ время, и
ассерт по подстроке в общем буфере. Обе формы дают и ложное красное, и **тихо-зелёное** — оно дороже.
**Деньги:** любое число из СЫРОГО леджера, а не из агрегата; леджер — нижняя граница.
**Шов:** платформа биллит пользователю `committed_usd` и просила у движка публикацию оценочных строк
(`PD-441`). Инвентарь её каналов — чтением ЧУЖОГО кода, не по памяти; правок в `platform/` НЕ делать.
**Индустриальный первоисточник прежде велосипеда:** `httptrace` — stdlib; если решишь иначе, отвергни с доводом.
## 7. Записка-план ДО работы
До первой правки — короткая записка в свою секцию журнала: что берёшь, в каком порядке, что считаешь
рискованным. После работы — сверка комплектности против §4 МЕХАНИЧЕСКИ (таблицей), не перечитыванием.
## 8. Заявление = команда
**Эрудиция НЕ улика:** утверждение о коде, провайдере или чужой зоне грунтуется `file:line`, цитатой или
замером — либо несёт пометку **«мнение»**. Каждое число и категорика — с командой, которой получены. Отрицательный замер печатается ВМЕСТЕ с
контрольной величиной. Сверх этого: **перед отчётом сверь каждый клейм с результатом инструмента ЭТОЙ
сессии**, а не с памятью и не с этим промтом — адреса §4 сняты 08.09 и могли уехать. **Последний абзац
отчёта оказался планом или обещанием? сделай это СЕЙЧАС.** ⛔ Править тест/гейт, чтобы он прошёл, —
НЕДОПУСТИМО; исключение — правка от ЗАКАЗАННОЙ смены поведения, и она объявляется отдельным пунктом.
Сессия длинная — поставь себе интервальную самоверификацию субагентом против ЯВНЫХ критериев §13.
## 9. Эхо-протокол старта
ДО работы — ≤10 строк **СВОИМИ словами**: скоуп · инварианты · что НЕ делаешь. Дословный пересказ промта
подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним. Эхо — оркестратору по адресу
из прямого канала (§12); канала нет — эхо первой секцией отчёта, работа продолжается.
## 10. Что НЕ удалось — обязательная секция отчёта
Что не вышло, что не проверено, где сомневаешься. «Не проверено» и «опровергнуто» — РАЗНЫЕ исходы.
## 11. Канал вопросов и право сказать «этого делать не надо»
Конфликт промта с кодом или доками — **пинг, а не интерпретация**: конструкция этого пака сошлась в
консилиуме двух моделей, но её проверяли чтением, а не твоим прогоном. Считаешь пункт неверным — скажи с
доводом, довод сильнее моей подписи. Решение владельца требуется, если работа упрётся в перекупку книг, в
скоуп или в продукт.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши СВОЙ блок в `/tmp/textmachine-channel` первым делом,
чужие блоки не трогай. ⚠ Адрес из файла не доказывает, что сессия жива: сверься с `ListAgents`. Нужной роли
нет или имя не отвечает ⇒ **канала нет, и это НОРМАЛЬНЫЙ случай: НЕ опрашивай сессии подряд** — вопрос
секцией в отчёт, работа продолжается.
## 13. Критерий завершённости — по нему тебя примут
Работу доводишь до конца САМ; приёмка идёт последней и вторым кругом твоей самопроверки не служит.
Готово — это когда:
- у КАЖДОГО пункта §4 есть исход: сделано · не делаю с доводом · пинг;
- круги СОШЛИСЬ: последний проход не дал НОВЫХ находок, прежние закрыты таблицей;
- **каждый класс закрыт со ЗНАМЕНАТЕЛЕМ «N из M»**, и M посчитан командой;
- ДЕВЯТЬ фикстур §5.1 существуют, утверждают ЧИСЛО вызовов у СЕРВЕРА и несут ПАРНОЕ утверждение о резюме;
пин резюма (§4.3) предъявлен мутацией; поля утверждены двусторонне;
- тест-тождество §4.4 пинит формулу, а не константу;
- числа сняты ПОСЛЕ последней правки, со счётом скипов рядом с числом `ok`;
- **таблица мутаций ПОЛНАЯ, и выжившие названы поимённо** (выжившая мутация — находка, а не шум);
- **дифф `^func Test` снят ИСПОЛНЕНИЕМ, а не памятью**, и назван прибор, которым считан;
- всё живое — в ДЕРЕВЕ, а не в письме;
- сказано явно: **«работа завершена, править не планирую»**. Без этой фразы пак считается идущим.
## Деньги
**Пак $0.** Платных вызовов провайдерам НОЛЬ: все девять исходов предъявляются на `httptest`-сервере,
формула дедлайна — арифметикой, резюм — фикстурой. ⛔ Платная проба вендорского баланса ОТКЛОНЕНА
владельцем (`D39.230` п.2) — не проектировать и не предлагать. Кажется, что нужен платный прогон, — это
стоп и пинг, а не вызов.
## Отчёт
Своя секция в `docs/PROGRESS.md`: исход по каждому пункту §4 · таблица «находка → что сделано → чем
предъявлено» · столбец «закрыт в N из M» · числа со счётом скипов · заказанные смены поведения отдельными
пунктами · пинги (квирк-реестр — полигону; публикация оценочных строк — платформе) · §10 «что не удалось» ·
вопросы оркестратору.

View file

@ -3,7 +3,7 @@
> **Единственный трекер проекта.** Здесь живут строки, на которые доки, промты и D-ноты ссылаются словами «строка N» / «строка бэклога N»: **ID строки стабилен навсегда**, не перенумеровывается и не переиспользуется (D39.80). Правки — только через оркестратора; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено.
> ⚠ **Это НЕ бэклог зоны `docs/`, а бэклог ПРОЕКТА.** У зон свои, с другими неймспейсами ID, и единый их строк не принимает (D39.84): платформа — [../platform/BACKLOG.md](../platform/BACKLOG.md) (`П-N`) и её регистр дефектов `platform/docs/DEFECT_REGISTER.md` (`PD-N`); фронт — [../frontend/docs/BACKLOG.md](../frontend/docs/BACKLOG.md) (`Ф-N`).
> ⚠ **Состояние проекта — не здесь.** Очередь, курс, CURRENT-STATE, состояние паков и живая хроника — шапка [PROGRESS.md](PROGRESS.md). Здесь только долг и его диспозиции.
> - **СЧЁТ ОЧЕРЕДИ на 08.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **262** строк · зона бэкенд **124** строго / **171** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 1** (236 закрыта D39.175), платные прогоны разблокированы · «скоро» **118** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже).
> - **СЧЁТ ОЧЕРЕДИ на 08.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **263** строк · зона бэкенд **125** строго / **172** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 1** (236 закрыта D39.175), платные прогоны разблокированы · «скоро» **118** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже).
> - ⚠ **Переезд 06.09 (D39.218):** таблица вынесена из `PROGRESS.md` ДОСЛОВНО и ни один ID не сдвинут — **с единственным исключением: две ячейки (строки 220 и 306) тронуты РОВНО в номере якоря `docs/scripts/counts.py:566`→`:569` и `:592`→`:595`**, потому что цель сдвинул этот же коммит; текст ячеек не менялся, длины равны. В `PROGRESS.md` на прежнем месте оставлен заголовок-указатель, поэтому ссылки «секция „Бэклог“» доезжают в один хоп.
## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора)
@ -298,3 +298,4 @@
| 367 | **ГАРАНТИЯ РЕЗЕРВА У ПОТОЛКА РАЗРЕЗОВ СУЩЕСТВУЕТ ТОЛЬКО ПОД КОНКУРЕНЦИЕЙ, А ТРИ КОММЕНТАРИЯ УТВЕРЖДАЮТ ОБРАТНОЕ.** `platform/internal/books/limit.go`, `takeCutSlot`: удачный `sem.TryAcquire(1)` возвращает слот СРАЗУ, не спросив `worthStarting` — то есть на свободном хосте слот выдаётся при любом остатке бюджета, хоть при миллисекундах. Проверка стоит только на пути ожидания. ⛔ Три места говорят иначе: «`reserve` is what a WON slot must still be worth» (`limit.go`), «a cut is not started … unless what remains is what this platform calls a cut's worth of time» (`parse.go`), «Below zero there is no cut to run, and it is not ATTEMPTED» (`books.go`). ⚠ Сегодня не кусает — у интейка `reserve = 0`, у очереди `JobTimeout` 15 мин, свип гарантирует запас, — но защита отсутствует ровно на пути, который проходится почти всегда, и **любой пин на неё зелёный навсегда**: он попадает только в ветку ожидания. Это тот самый класс вырожденного пина, против которого построена вся смена. ⇒ лечение скорее всего однострочное (спросить `worthStarting` ДО `TryAcquire`), но форма и цена — зоне. ⚠ Не взято дофиксом сознательно: правило остановки по этому паку объявлено ДО находки, и нарушать его задним числом значит обесценить сам механизм остановки | платформа | скоро | гарантия резерва действует на ОБОИХ путях ЛИБО три комментария приведены к правде; пин достигает быстрого пути | охотник вне карты, приёмка пака разреза приёма 08.09 |
| 368 | **ОПЕРАТОРСКИЙ ЭКРАН ЗАНИЖАЕТ ОЦЕНОЧНЫЕ ТОКЕНЫ В 5 РАЗ.** `est_tokens` в `request_log` считается `r.estOutTokens(ch.Text)` по фертильности видимого перевода, а у DeepSeek размышление сидит ВНУТРИ `completion_tokens` ⇒ на строках `decode_error` стоит 2887/2854/3162, тогда как соседние попытки тех же чанков реально дали **16 000** (`finish=length`). Занижение **5.05.6×**. Читатель — `cmd/tmctl/render.go` («estimated-cost rows: N … ~M est. output tok»): оператор видит впятеро меньше правды. ⚠ В `cost_usd` это число НЕ входит — деньги считаются грантом, — то есть врёт легенда, а не счёт; но врёт она ровно тому, кто по ней решает, продолжать ли платить. Класс «прибор отвечает на свой вопрос» (`D39.202`). Находка консилиума с чистым Fable 5, 08.09 | бэкенд | скоро | легенда считает то же, что считает счёт, либо честно говорит, чего не знает | консилиум по строке 360, 08.09 |
| 369 | ⛔ **ОСЬ ЭСКАЛАЦИИ D2 СЕГОДНЯ ФИКТИВНА: БРОНИРУЕТСЯ 32 000 ТОКЕНОВ, ДОСТИЖИМО ~19 400.** Реген удваивает `max_tokens` 16 000 → 32 000 и НЕ трогает время, а `attempt_s: 240` при 7090 tok/s даёт потолок **1721 тыс. токенов**. Замер по 20 регенам трёх DeepSeek-армов (`docs/experiments/23-editor-tier.md` + леджеры `probe-4axes/arms/`): доставлено первой попыткой 17, обрезано 3; максимум, когда-либо доставленный регеном — **19 354 = 60 % гранта**; **13 доставок из 17 уложились бы в ИСХОДНЫЙ грант 16 000** ⇒ реген «срабатывает» пере-броском длины размышления, а не бо́льшим бюджетом. ⚠ Цена не только в обрывах: резервация бронирует полные 32 000 ($0.127 выхода) при досягаемых ~$0.08 — фикция съедает headroom под книжным потолком. ⇒ дедлайн от бюджета (строка **360**) — не подкрутка таймаута, а то, что делает эту ось настоящей; закрывать вместе с ней. Категорика, которая держится: реген не может использовать верхние ~40 % выданного, и всякий, кому эскалация РЕАЛЬНО нужна, обрезается по построению | бэкенд | скоро | эскалация досягаема по времени либо не бронирует недостижимое | консилиум по строке 360, 08.09 |
| 370 | **Anthropic-адаптер не несёт капа ретраев, который есть у общего транспорта.** `backend/internal/llm/provider_anthropic.go` возвращает `BilledDecodeError` с `retryable=true` без ограничителя `billedDecodeSeen`, который стоит в `httpllm.go` — то есть на этом адаптере оплаченный 2xx может повториться до `max_attempts`, а не один раз. ⚠ **Боевой цены сегодня НОЛЬ:** поверхность deprecated, Anthropic убран из стека 04.07, вызовов по этому пути в прогонах нет. ⇒ сознательно вынесено ИЗ пака «вызов, который оборвали мы» (строка 360) по доводу ревью: пак получил бы тесты и мутации на мёртвую поверхность. Чинить при воскрешении адаптера — тогда же и проверять, применим ли к нему признак доставки | бэкенд | когда-нибудь | адаптер получает тот же кап ЛИБО удалён вместе с поверхностью | ревью пака «вызов, который оборвали мы», 08.09 |

View file

@ -23,6 +23,7 @@
| Роль | Активный промт | Статус |
|---|---|---|
| Оркестратор | [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) | роль и нормы; счётчик роли — CURRENT-STATE |
| **Бэкенд · ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ** | [BACKEND_CUT_CALLS_SESSION_PROMPT.md](BACKEND_CUT_CALLS_SESSION_PROMPT.md) | ⚠ **ВЫДАН 08.09, $0.** Движок платит за генерации, которые сам же оборвал: ошибка чтения выбрасывается, обрыв распознаётся по РАЗМЕРУ, оборванное нами тело ретраится. Спина — принцип, к которому сошёлся консилиум: граница денег есть факт ДОСТАВКИ запроса, а не статус-строка. Строки **360** и **369**, попутно **78**. Слово владельца — `D39.230`: списывать по оценке, пометка есть УСЛОВИЕ, платной пробы нет, ожидание разрешено. ⚠ Прошёл механическую сверку, опровергателя (5 блокеров закрыто ДО выдачи, включая невозможность в самой конструкции) и ревью старшей модели |
| Бэкенд · ДВИЖОК УЖЕ ЗНАЕТ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДВУМЯ ДОФИКСАМИ 08.09** — акт **D39.228** (`2b6f1f4`, 21 путь); промт в `archive/prompts/` с баннером-исходом. Закрыты строки 355 · 357 · 358; у **253** и **312** закрыта ДВИЖКОВАЯ половина, платформенная жива (испр. 08.09 по кросс-модельному ревью: акт объявил обе закрытыми целиком). Живое из не взятого — **362** (ремонт против ремонта, 77 % денег чанка) и **363** (текстовый сайдкар молчит о неполноте); **360** ждёт слова владельца по денежной половине |
| Бэкенд · КОНСИСТЕНТНОСТЬ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 07.09** — акт **D39.222**; промт в `archive/prompts/` с баннером исхода. Строки 267 и 318 закрыты, у 320 исполнена половина (а). Живой остаток зоны ЗАКРЫТ актом **D39.226** (строки 333338); живое — 353359 |
| Бэкенд · ПРАВДА ОПЕРАТОРУ И ДЕНЬГИ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 08.09** — акт **D39.226**, живой платный прогон; промт в `archive/prompts/` с баннером-исходом |