From 60d575164df072c39bc7133bf4b7ec69a35c9ea4 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 8 Sep 2026 17:48:14 +0300 Subject: [PATCH] Hand the engine the calls it cuts itself: the money boundary is delivery of the request, not the status line --- docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md | 344 +++++++++++++++++++++++ docs/BACKLOG.md | 3 +- docs/README.md | 1 + 3 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md diff --git a/docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md b/docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md new file mode 100644 index 00000000..87dc5164 --- /dev/null +++ b/docs/BACKEND_CUT_CALLS_SESSION_PROMPT.md @@ -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 держит 75–76 tok/s (n=139). Грант +редактора 16 000 ≈ 213 с; реген удваивает его до 32 000 ≈ **427 с** и НЕ трогает время, а `attempt_s` +равен 240. Потолок по времени 17–21 тыс. токенов; максимум, когда-либо доставленный регеном, — **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 «что не удалось» · +вопросы оркестратору. diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 2ed0270a..1c739395 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -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.0–5.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` при 70–90 tok/s даёт потолок **17–21 тыс. токенов**. Замер по 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 | diff --git a/docs/README.md b/docs/README.md index e2503b94..51a14f55 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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** (строки 333–338); живое — 353–359 | | Бэкенд · ПРАВДА ОПЕРАТОРУ И ДЕНЬГИ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 08.09** — акт **D39.226**, живой платный прогон; промт в `archive/prompts/` с баннером-исходом |