Take the review order back out of the tree: it is a session scratchpad, not repository evidence

This commit is contained in:
heaven 2026-09-09 00:32:17 +03:00
parent 3a6d1dd021
commit 7aa31ceddd

View file

@ -1,457 +0,0 @@
> ⚠ **РЕВЬЮ-ШАПКА ОРКЕСТРАТОРА (перенос в репозиторий 08.09).** Это наряд СЕДЬМОГО адверсариального круга
> движковой сессии `textmachine-79` по её же дофиксу пака «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ». Перенесён
> ДОСЛОВНО и без правок: он жил в `/tmp` и умер бы вместе с сессией, а по канону доказательная база
> пака живёт в репозитории. **Ничего из него НЕ ратифицировано** — это утверждения линз сессии, из
> которых оркестратором пере-проверено ТРИ (потеря денег сожжённых ключей подтверждена чтением
> `stagerun.go`). Остальные держатся уликами наряда, не моим замером.
> ⛔ **Работа по этому наряду НЕ заландена и на момент переноса не начата.** Дерево — 23 пути в
> `backend/`, незакоммичено; патч и новые файлы сохранены отдельно. Батарея и мутации на нём ЗЕЛЁНЫЕ,
> и пятнадцать денежных находок живут при этой зелени — это и есть главный факт документа.
> ⚠ **Сырьё воркфлоу (13 лент агентов, 380790 КБ каждая) НЕ перенесено** — оно в каталоге сессии и
> эфемерно; карта «лента → роль → предмет» есть в конце наряда, но по ней уже не пройти после смерти
> сессии. Это осознанная потеря: в репозиторий кладётся дистиллят, а не семь мегабайт лент.
> Носитель решения о судьбе работы — строка бэклога **360** и слово владельца.
# НАРЯД: 35 находок седьмого круга по дофиксу пака «ВЫЗОВ, КОТОРЫЙ ОБОРВАЛИ МЫ»
**Файл — рабочий скратчпад сессии textmachine-79, НЕ часть репозитория.** Всё ниже извлечено ПРОГРАММНО
из журнала воркфлоу, а не пересказано по памяти.
- источник: `~/.claude/projects/…/subagents/workflows/wf_c3ebfa7b-5d9/journal.jsonl` (13 агентов)
- сырьё: `scratchpad/lens-raw.json` · генератор: `scratchpad/gen-order.py`
- спаривание находка↔вердикт — ПОЗИЦИОННОЕ внутри линзы; проверено: из 36 пар 13 совпали и по заголовку,
остальные 23 — та же находка с сокращённым заголовком опровергателя (сверено глазами по выводу)
**Заявлено линзами 36 → пережило опровержение 29. Плюс 6 у критика полноты (опровергателя у него НЕ было).**
## Счёт по диспозиции
| приоритет | что значит | штук | из них уникальных правок |
|---|---|---|---|
| **P0** | деньги теряются или списываются неверно | 12 | 9 |
| **P1** | число, пин или гейт врёт | 12 | 11 |
| **P2** | слово оператору врёт | 7 | 6 |
| **PING** | не моё решение — оркестратору и владельцу | 4 | 4 |
| | **ВСЕГО** | **35** | **30** |
⚠ «Уникальных» меньше, потому что часть находок разных линз указывает на ОДНУ правку — они помечены
«дубль» и чинятся вместе. Дубли НЕ выброшены: каждая оставлена со своим ключом, чтобы при проверке
было видно, что ни одна не потеряна.
## ЧТО ЧИНЮ — по приоритету, в порядке работы
### P0 · `bank-and-counters#1` — Ожог в банковой партии режет УЖЕ ОПЛАЧЕННЫЕ партии: проход, стоящий $0.000000, дропается целиком
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/terminologist.go:947-961`
- **как:** Префиксный `break` роняет УЖЕ ОПЛАЧЕННЫЕ партии, когда одна из ранних сожжена: проход, стоящий $0, дропается целиком. Пропускать сожжённую, а не обрывать префикс.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Дофикс научил зонд отвечать «не оплачено» на ожоге — верно. Но отказ попадает в ПРЕФИКСНЫЙ цикл: первая непоместившаяся партия делает `break`, и `fits` остаётся границей ПРЕФИКСА, а не счётчиком. Партия с ожогом на попытке 0 теперь требует бюджет — и уносит с собой все последующие партии, включая честно оплаченные, чей реплей стоит НОЛЬ. Собственный комментарий цикла (terminologist.go:941-942) обещает обратное: «Already-paid batches cost nothing and are admitted regardless — holding them back would save no money and lose their result». Соседний ремонтный проход в этой же ситуации делает `continue` (repair.go:299), а не `break`. Комментарий боевого конфига называет ровно этот класс: configs/pipeline-c1.yaml:166 «бюджет, режущий ОПЛАЧЕННУЮ работу». Побочно: `paidBatch` (terminologist.go:944,951) ЗАПИСЫВАЕТСЯ и НИКОГДА не читается (2 упоминания на файл) — это и есть тот срез, который позвол
- **улика линзы:** Проба на копии, две партии, budget_usd=$0.000001. CONTROL answered/answered budget=$0.000001: err=<nil> attempted=2 dropped=0 texts=["термин\tterm" "термин\tterm"] provider_calls=0 SUBJECT burned/answered budget=$0.000001: err=<nil> attempted=0 dropped=2 texts=["" ""] provider_calls=0 ORDER answered/burned budget=$0.000001: err=<nil> attempted=1 dropped=1 texts=["термин\tterm" ""] provider_calls=0 Контроль показывает, что крошечный бюджет сам по себе оплаченный проход НЕ останавливает (attempted=2), а перестановка ожога в конец списка спасает оплаченную партию (attempted=1) — значит причина именно префиксный `break`, а не бюджет. Пре-дофиксная форма зонда (`return cp != nil, err`) на том же
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ своим прогоном на копии (копия сверена с исходником: 8 из 8 файлов предмета SAME). terminologist.go:944-965 — `fits` это ГРАНИЦА ПРЕФИКСА (`break` на 960, потребитель `for i := 0; i < fits; i++` на 971), а собственный комментарий цикла на 941-942 обещает «Already-paid batches cost nothing and are admitted regardless». Мой прогон, budget=$0.000001, две партии: CONTROL answered/answered attempted=2 dropped=0 texts=["термин\tterm" "термин\tterm"] cost=0.000000 provider_calls=0 SUBJECT burned/answered attempted=0 dropped=2 texts=["" ""] cost=0.000000 provider_calls=0 ORDER answered/burned attempted=1 dropped=1 texts=["термин\tterm" ""] cost=0.000000 provider_calls=0 Контроль (обе опл
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/pipeline_zz_probe_test.go "$d/backend/internal/pipeline/zz_probe_test.go" && cd "$d/backend" && g`
### P0 · `bank-and-counters#3` — Два ДРУГИХ зонда «чекпойнт есть ⇒ уже оплачено» про ожог не знают: перепокупка уходит мимо суб-бюджета эскалации и ремонта
- **действие:** ЧИНЮ
- **где:** `escalation.go:146, repair.go:368`
- **как:** дубль burn-walk#2 и #3, чинится теми же двумя правками
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Ответ на п.5: производственных вызывающих `GetCheckpoint(` — 10; восемь читают ТЕКСТ результата (по `cs.FinalHash` — export.go:482, resume.go:34, resume.go:60, repin.go:243, quality.go:365) либо уже знают про ожог (stagerun.go:488 — сам обход, stagerun.go:506 — за обходом, terminologist.go:792 — починен). Остаются ДВА зонда ровно той формы, которую дофикс чинил в банке. (а) Эскалация: ожог на ключе хопа даёт `mayHop=true`, и ветка `if !mayHop` пропускается ЦЕЛИКОМ — вместе с `escalationBudgetRemains()` и с `escMu.Lock()` (escalation.go:155-156). Дальше runAttempt перешагивает ожог и покупает СВЕЖИЙ хоп: вне бюджета эскалации и вне мьютекса, который комментарий на 148-154 держит именно ради «overshoot ≤ 1 hop». Причём ожог на ключе хопа — не гипотеза: комментарий stagerun.go:472-476 называет остановленный хоп мотивирующим случаем всего п.2. (б) Ремонт: `paid=true` на ожоге пропускает весь
- **улика линзы:** Проба с контролем пустого ключа (обе строки — один прогон): repairCheckpointExists: empty_key=false(err=<nil>) BURNED_key=true(err=<nil>) burnedByCut(row)=true escalation fbExists: empty_key_nil=true BURNED_key_mayHop=true burnedByCut(row)=true Контроль (пустой ключ → false/nil) доказывает, что прибор спрашивает существующий предмет, а не всегда отвечает «да».
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ, и эскалацию — СКВОЗНЫМ прогоном с деньгами. escalation.go:142-147 `mayHop := fbExists != nil`; repair.go:364-368 `return cp != nil, err`обе формы дофикс в банке чинил, здесь не тронул. (а) Эскалация, оба ряда одним прогоном, escalation.budget_usd=0 (то есть гейт запрещает ЛЮБОЙ хоп — escalationBudgetRemains возвращает false при budget<=0, escalation.go:92-95): CONTROL empty hop key esc_budget=0: attempted=false err=<nil> fresh=false runCost=0.000000 provider_calls=0 escalation_spent=0.000000 SUBJECT burned hop key esc_budget=0: attempted=true err=<nil> fresh=true runCost=0.002000 provider_calls=1 escalation_spent=0.002000 Контроль печатает ноль вызовов — значит прибор спрашив
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/pipeline_zz_probe_test.go "$d/backend/internal/pipeline/zz_probe_test.go" && cd "$d/backend" && g`
### P0 · `bank-and-counters#4` — Доставленный обрыв ТЕРЯЕТСЯ на обоих выходах retryLoop по отмене: до вызывающего не доходит ни Deliveries, ни сам обрыв
- **действие:** ЧИНЮ
- **где:** `internal/llm/httpllm.go:211 и :234`
- **как:** дубль money-predicate#1: оба выхода по отмене зовут cancelledDuring БЕЗ withEarlierCut, поэтому доставленный обрыв до вызывающего не доходит.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Ответ на п.2 в его сильной форме. `Deliveries` действительно никогда не бывает 0 у дошедшей ошибки (замер ниже), но есть путь, где ошибка обрыва НЕ ДОХОДИТ ВООБЩЕ, и вниз по течению это читается ровно как «доставок не было». `firstCut` (httpllm.go:193, комментарий 187-192 объясняет, зачем он заведён) приклеивается только на выходах 214 и 238; на двух выходах по отмене — 211 и 234 — он молча отбрасывается. Цепочка «доставленный обрыв → обычный 500 → стоп во время бэкоффа» возвращает голую отмену: stagerun.go:723 `errors.As(err,&cut)` не находит ничего, идёт в ветку release, книжит $0 за сгенерированный ответ, а `recordCancelledStage` (cutcall.go:164, требует `CutByParent && Delivered`) не пишет метку. Это тот самый дефект, который `firstCut` заведён устранять.
- **улика линзы:** LOST-CUT PROBE: calls=2 errors.Is(canceled)=true errors.As(cut)=false err=context canceled CONTROL (no cancel): calls=3 errors.As(cut)=true cause=connection_lost deliveries=1 err=p: exhausted 3 attempts: p http 500: … / p: delivered request cut by connection_lost after 21 body bytes: unexpected EOF Тот же фикстур и тот же первый обрыв: без отмены обрыв доживает до вызывающего (контроль), с отменой во время бэкоффа — исчезает.
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ денежную половину; вторую половину находки — про метку — ОПРОВЕРГ. Подтверждено: `firstCut` (httpllm.go:193, мотив в комментарии 187-192) приклеивается только на 214 и 238; на 211 и 234 стоит голый `cancelledDuring(...)`, а cancelledDuring (attemptcut.go:326-334) джойнит ТОЛЬКО последнюю ошибку. Прогон, один фикстур, два ряда: CONTROL (no cancel) calls=3 errors.Is(canceled)=false errors.As(cut)=true cause="connection_lost" deliveries=1 billable=true err=p: exhausted 3 attempts: p http 500: {"error":"boom"} SUBJECT (cancel in backoff) calls=2 errors.Is(canceled)=true errors.As(cut)=false cause="" deliveries=0 billable=false err=context canceled Контроль доказывает, что тот же перв
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/llm_zz_probe_test.go "$d/backend/internal/llm/zz_probe_test.go" && cd "$d/backend" && go test ./i`
### P0 · `burn-walk#1` — Деньги сожжённых ключей ТЕРЯЮТСЯ на свежем вызове: burnedCost перезаписывается, а не прибавляется
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:784 и :713`
- **как:** В обеих ветках писать `burnedCost + cost` / `burnedCost + estimate` вместо голого присваивания. Пин: прогон с ДВУМЯ сожжёнными ключами подряд, сверка chunk_status.cost_usd с SUM(checkpoints.cost_usd) SQL-запросом.
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** Обещание накопителя выполнено в 3 из 5 точек. stagerun.go:501 `att.cumCost = burnedCost`, 518 `att.cumCost = burnedCost + cp.CostUSD`, cutcall.go:110 `att.cumCost, att.runCost = att.cumCost+cost, cost` — несут. А два выхода СВЕЖЕГО вызова присваивают, а не прибавляют: 784 `att.cumCost, att.runCost = cost, cost` и 713 `att.cumCost, att.runCost = estimate, estimate`. Итог: как только после обхода сожжённых ключей вызов УДАЁТСЯ (или падает billed-decode), их деньги исчезают из att.cumCost → из cumCost петли (171) → из chunk_status.cost_usd. Это прямо противоречит собственному комментарию обхода («The money of every key it steps over stays in the chunk's honest total», 478-480), комментарию cutcall.go:107-109 («dropping it would under-state a position that was cut twice») и контракту поля store/chunkstatus.go:32 «CostUSD // sum across all attempts (F3-honest)». Двойного счёта нет нигде (пров
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeTwoBurnedKeysThenAFreshAnswer -v (два стопа, затем здоровый прогон): checkpoint attempt=0 cost=0.00105600 finish="cancelled" checkpoint attempt=1 cost=0.00105600 finish="cancelled" checkpoint attempt=2 cost=0.00200000 finish="stop" healthy run: fresh_calls=1 | SUM(checkpoints)=0.00411200 rows=3 | chunk_status cost=0.00200000 rows=1 FAIL: the chunk's honest total lost the burned money: chunk_status.cost_usd=0.00200000, SUM(checkpoints.cost_usd)=0.00411200, gap=0.00211200 То же против самого ledger'а — go test -run TestProbeCommittedVersusTheRowsTheOperatorReads -v: committed=$0.00411200 | paidTail.TotalUSD=$0.00411200 (superseded $0.00211200, shipped
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СВОИМ ПРОГОНОМ, но тяжесть исходной оценки завышена. Линии сверены дословно в копии дерева: stagerun.go:501 `att.cumCost = burnedCost`, :518 `att.cumCost = burnedCost + cp.CostUSD`, cutcall.go:110 `att.cumCost, att.runCost = att.cumCost+cost, cost` — несут; stagerun.go:713 `att.cumCost, att.runCost = estimate, estimate` и :784 `att.cumCost, att.runCost = cost, cost` — присваивают. Путь достижим в САМОМ центральном сценарии пака (стоп → резюме), а не в экзотике. Мой прогон (probe A, стоп оператора на attempt 0, затем здоровый провайдер): run1 chunk_status: disp=flagged flag=cancelled attempts=0 cost=0.00105600 run2: «attempt was paid for and cut off … re-asking under a fresh key»
- **воспроизведение:** `cd /home/ubuntu-26/projects/textmachine/backend && d=$(mktemp -d) && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' . && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/zzprobe_test.go "$d/backend/internal/pipeline/" && (cd "$d/backend" && go test ./internal/pipeline/ -run T`
### P0 · `burn-walk#2` — Сожжённый ключ РЕМОНТА читается как «уже оплачено» — суб-бюджет repair обходится целиком
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/repair.go:368`
- **как:** Зонд «чекпойнт есть ⇒ оплачено» обязан видеть ожог: `cp != nil && !burnedByCut(cp)`. Ровно та правка, что уже сделана для банка (terminologist.go). Пин — по образцу TestTheBankPaidProbeSeesThroughABurnedCheckpoint, на зонде, а не на предикате.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** `return cp != nil, err`. Банковский зонд в этом же дофиксе (пункт 7) получил `cp != nil && !burnedByCut(cp)` (terminologist.go:793) вместе с ⛔-комментарием, дословно описывающим механику: «runAttempt walks past it and buys the batch again — and a probe answering «already paid» would let that purchase escape the role sub-budget». Ремонтный зонд той же формы правку НЕ получил. Последствие в repair.go:286-298: `paid, _ := r.repairCheckpointExists(...)`; ворота цены стоят внутри `if !paid { ... spent+want > gate.BudgetUSD ... }`. Сожжённый ключ ⇒ paid=true ⇒ ворота не спрошены ⇒ runAttempt шагает мимо сожжённого ключа и покупает СВЕЖИЙ вызов вне gates.repair.budget_usd.
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeABurnedRepairKeyBypassesTheRepairBudget -v (прогон 1 — сокет ремонта рвётся после заголовков; прогон 2 — здоровый провайдер, бюджет вдвое меньше одного вызова): checkpoint attempt=0 role=repair cost=0.001063 finish="connection_lost" text_len=0 after run 1: repair-role spend=$0.001063, one repair call estimates $0.001063, burned checkpoints=1, checkpoints total=3 repair max_tokens: the cut call asked 512, the re-done one asked 512 run 2: budget=$0.000531 (already spent $0.001063 on the repair role), fresh repair calls bought=1 FAIL: a repair call was bought under a budget of $0.000531 that one $0.001063 call cannot fit… КОНТРОЛЬ (тот же бюджет, но бе
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СВОИМ ПРОГОНОМ. repair.go:368 `return cp != nil, err` против terminologist.go:793 `return cp != nil && !burnedByCut(cp), err` — банковский зонд правку дофикса получил, ремонтный той же формы нет. Ворота цены в repair.go:286-298 стоят внутри `if !paid { … spent+want > gate.BudgetUSD … }`, то есть paid=true снимает вопрос о деньгах целиком. Мой прогон (probe C: run 1 — сокет ремонта рвётся ПОСЛЕ заголовков, значит cut.Billable; run 2 — здоровый провайдер, бюджет вдвое меньше одного вызова): run1 checkpoint: stage=edit role=repair cost=0.00106300 run1 err=… delivered request cut by connection_lost after 24 body bytes … | repair calls on the wire=2 | repair-role checkpoints=1 | one
- **воспроизведение:** `cd /home/ubuntu-26/projects/textmachine/backend && d=$(mktemp -d) && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' . && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/zzprobe_test.go "$d/backend/internal/pipeline/" && (cd "$d/backend" && go test ./internal/pipeline/ -run '`
### P0 · `burn-walk#3` — Сожжённый ключ ХОПА читается как «уже оплачено» — escalation.budget_usd не спрашивается (и escMu не берётся)
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/escalation.go:146`
- **как:** То же для `mayHop := fbExists != nil`. Дополнительно проверить, что escMu берётся, раз хоп реально пойдёт на провод.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** `mayHop := fbExists != nil` — «чекпойнт на ключе хопа есть ⇒ он оплачен, переиграть бесплатно». Сожжённый чекпойнт — это деньги без результата: runAttempt шагает мимо и покупает СВЕЖИЙ хоп, но ветка, которая его допустила, пропустила и `escalationBudgetRemains()` (строки 154-160), и `r.escMu.Lock()` — то есть и бюджет эскалации, и сериализацию, которой комментарий ограничивает перерасход «≤ 1 хопа» при N параллельных воркерах. Крайний случай: budget_usd=0 (эскалация ВЫКЛЮЧЕНА, объявленный боевой дефолт) — хоп всё равно покупается.
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeABurnedHopKeyBypassesTheEscalationBudget -v: run 1 (budget $5.00): err=… cut by connection_lost … | server calls=3, of them hop=2 checkpoint attempt=0 role=translator cost=0.001056 finish="connection_lost" text_len=0 run 2 (budget $0.00 — escalation OFF): server calls=1, of them hop=1 FAIL: a fresh escalation hop was bought with escalation.budget_usd=0… КОНТРОЛЬ (чистый проект, тот же budget_usd=0, сожжённого ключа нет) — TestProbeControlBudgetZeroBuysNoHop: PASS, «control: server calls=1, of them hop=0» (то есть вызов был, а хопа не было — прибор не молчит). ПРИЧИННОСТЬ: sed 146 → `fbExists != nil && !burnedByCut(fbExists)` ⇒ проба PASS («run 2 … o
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СВОИМ ПРОГОНОМ, и хуже, чем «просто бюджет». escalation.go:146 `mayHop := fbExists != nil`; всё, что защищает деньги, живёт в ветке `if !mayHop` ниже — `r.escMu.Lock()` (сериализация, которой комментарий ограничивает перерасход «≤1 хопа» при N воркерах) и `escalationBudgetRemains()` (escalation.go:91-101, `budget <= 0 → false`). Сожжённый чекпойнт — деньги без результата, runAttempt шагает мимо него (stagerun.go:492) и покупает СВЕЖИЙ хоп, минуя обе защиты. Мой прогон (probe B: run 1 — первичный ответ content_filter (эскалируемый), хоп доставлен и оборван стопом оператора; затем escalation.budget_usd переписан в 0.0 — это НЕ вход снапшота, снапшот не двигается, буквально резюме)
- **воспроизведение:** `cd /home/ubuntu-26/projects/textmachine/backend && d=$(mktemp -d) && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' . && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/zzprobe_test.go "$d/backend/internal/pipeline/" && (cd "$d/backend" && go test ./internal/pipeline/ -run '`
### P0 · `cancelled-mark#1` — Метка над эскалационным хопом не несёт денег хопа: ledger 0.001176, строка 0.000120
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:233-240 + escalation.go:173`
- **как:** Деньги хопа прибавлять к cumCost ДО возврата по ошибке (сейчас только внутри `if esc.attempted`, то есть после `if err != nil { return }`). Пин: отмена над хопом, сверка chunk_status.cost_usd с леджером.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Дофикс закрыл «строки нет вовсе», но не «строка врёт числом». Деньги хопа в `cumCost` не попадают ДВАЖДЫ: (а) `return nil, err` на stagerun.go:237 стоит ПЕРЕД `cumCost += esc.fb.cumCost` на строке 240; (б) даже если поменять порядок — `maybeEscalate` на escalation.go:173 отдаёт `return out, err` с НУЛЕВЫМ `out`, поле `out.fb` на ошибочной ветке не заполняется никогда, хотя `fb` от runAttempt уже несёт улаженную сумму. Побочно теряется и телеметрия: строка выходит `escalated=false`, `esc_model=""`, хотя хоп был вызван и оплачен. Следствие в деньгах, а не только в отчёте: `repricer.usd` (reprice.go:200-224) идёт по вызовам новейшими-первыми внутри `cs.CostUSD`, ловит перебор и возвращает заниженную сумму строки, помечая её `fromHistory` — это число уходит в `ProjectedBookUSD` (rebill.go:283, status.go:920) и в согласие на пере-оплату (rebill.go:244).
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeHopStopMarkCarriesTheHopsMoney (3 прогона подряд, идентично): CONTROL: committed=0.00117600 reserved=0.00000000 checkpoints=2 chunk_status_rows=1 CONTROL checkpoint: ch1/chunk0/draft role=translator esc=false cost=0.00012000 CONTROL checkpoint: ch1/chunk0/draft role=translator esc=true cost=0.00105600 CONTROL row: stage=draft disp=flagged flag=cancelled attempts=1 cost_usd=0.00012000 escalated=false esc_model="" CONTROL repricer quote for this position: usd=0.00012000 fromHistory=true moved=false (the calls on file total 0.00117600) MARK UNDER-STATES THE MONEY: ledger committed=0.00117600, the row it points at says cost_usd=0.00012000 (short by 0.00
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО своим прогоном, 3 из 3 идентичны. Опровергнуть не удалось ни по одному из четырёх каналов. (1) Код: stagerun.go:236-240 — `if err != nil { return nil, err }` стоит перед `cumCost += esc.fb.cumCost`; escalation.go:124 `var out escalationOutcome`, :172-173 `if !errors.Is(err, errReserveCeiling) { return out, err }` — fb действительно теряется на ошибочной ветке всегда. (2) Замер (мой fixture, не чужой: setupHopProject + early200 у хопа с задержкой 300 мс, чтобы 200 гарантированно был в руках клиента до стопа): CONTROL: server calls=2 committed=0.00117600 checkpoints=2 chunk_status_rows=1 / checkpoint esc=false cost=0.00012000 / checkpoint esc=true cost=0.00105600 / row: stage=draf
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/zzprobe_adv_test.go "$d/backend/internal/pipeline/" && cd "$d/backend" && go test ./internal/pipeline/ -`
### P0 · `cancelled-mark#2` — Тот же провал на ремонте: ledger 0.001303, сумма всех строк 0.000240
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:281-287 + repair.go:302-316`
- **как:** То же для ремонта: замер линзы — леджер 0.001303, сумма строк 0.000240.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Форма идентична хоповой и тоже двойная: `return nil, rerr` на stagerun.go:283 стоит перед `repairRes = rr` (285) и `cumCost += rr.CumUSD` (286), а сам `maybeRepair` на repair.go:312 возвращает `res` ДО строк 314-316 (`res.Calls++`, `res.CostUSD += att.runCost`, `res.CumUSD += att.cumCost`). Теряются не только деньги оборванного ремонт-вызова, но и деньги уже УСПЕШНЫХ ремонт-вызовов той же единицы, накопленные в `res` до обрыва: runStage выбрасывает весь `rr`. Вдобавок стадия, чей собственный вызов завершился `ok` и оплачен, уезжает в строку `flagged/cancelled` с `final_hash=""` — оборвали опциональный подшаг, а помечена вся позиция.
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeRepairStopMarkCarriesTheRepairsMoney (3 прогона подряд, идентично): CONTROL: server calls=3 committed=0.00130300 reserved=0.00000000 checkpoints=3 rows=2 CONTROL checkpoint: ch1/chunk0/draft role=translator cost=0.00012000 CONTROL checkpoint: ch1/chunk0/edit role=editor cost=0.00012000 CONTROL checkpoint: ch1/chunk0/edit role=repair cost=0.00106300 CONTROL row: stage=draft disp=ok flag= attempts=1 cost_usd=0.00012000 final_hash=6dea0970e4af7e71ef1f CONTROL row: stage=edit disp=flagged flag=cancelled attempts=1 cost_usd=0.00012000 final_hash= THE MARKED POSITIONS UNDER-STATE THE MONEY: ledger committed=0.00130300, chunk_status rows sum to 0.00024000
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО своим прогоном, 3 из 3 идентичны, числа сошлись с исходной находкой до знака. Код: repair.go:312 `return finalText, res, err` стоит ПЕРЕД :314-316 (`res.Calls++`, `res.CostUSD += att.runCost`, `res.CumUSD += att.cumCost`), а stagerun.go:282-287 — `return nil, rerr` перед `repairRes = rr` / `cumCost += rr.CumUSD`, то есть выбрасывается и `res`, уже несущий деньги УСПЕШНЫХ ремонт-вызовов той же единицы. Мой замер (свой fixture поверх setupRepairProject: обычные ответы на draft/edit, early200+300 мс+hold на ремонт-вызове, стоп): CONTROL: server calls=3 committed=0.00130300 checkpoints=3 rows=2 / checkpoint ch1/chunk0/draft role=translator 0.00012000 / ch1/chunk0/edit role=editor 0.
- **воспроизведение:** `# та же копия и тот же скопированный probe-файл, что в п.1: cd "$d/backend" && go test ./internal/pipeline/ -run TestProbeAdvRepairStopMoney -count=1 -v -timeout 120s 2>&1 | grep -E 'CONTROL|--- |UNDER-STATE' # контроль достижимости: grep -rn 'repair' configs/ | grep -v langpacks → 0 строк`
### P0 · `money-predicate#1` — Оплаченный обрыв теряется целиком, если между ним и остановкой был ЛЮБОЙ не-cut ретраибл (503/429)
- **действие:** ЧИНЮ
- **где:** `internal/llm/httpllm.go:211 и :234`
- **как:** Оплаченный обрыв теряется, если между ним и концом цепочки был ЛЮБОЙ не-cut ретраибл (503/429). Причина та же, что у #2: firstCut не доносится. Чинить вместе с money-predicate#2 и bank#4 — это одна семья из трёх выходов.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** `firstCut` копится на :207, но приклеивается к финальной ошибке только на двух выходах из четырёх — :214 и :238 (`withEarlierCut`). Оба выхода по отмене — :211 `cancelledDuring(ctx.Err(), err)` и :234 `cancelledDuring(ctx.Err(), lastErr)` — берут ошибку ПОСЛЕДНЕЙ попытки и `firstCut` не спрашивают. Цепочка «2xx, тело оборвано RST (Billable=true, retryable) → 503 → оператор жмёт стоп в бэкоффе» возвращает голый `context canceled`: `errors.As` не находит AttemptCutError вообще, stagerun.go:724 уходит в ветку «запрос не ушёл», освобождает резервацию, книжит $0, чекпойнт НЕ пишет и chunk_status НЕ пишет. Это дословно тот дефект, который комментарий на httpllm.go:226-233 объявляет закрытым («Measured: a delivered connection_lost plus a stop inside the backoff booked $0 and wrote no chunk_status row»). Пин TestAStopDuringABackoffKeepsTheEvidence (attemptcut_test.go:598) структурно не может это
- **улика линзы:** Транспортный уровень (internal/llm/zzprobe_cancelmask_test.go, фикстура: вызов1 = 200 + 41 байт тела + RST, вызов2 = 503, cancel через 150 мс после прихода второго): PROBE STOP-MASK: calls=2 cut survives the stop = false Billable = false err: context canceled (контроли теста: calls==2 и errors.Is(err, context.Canceled) — иначе t.Fatalf) Деньги, тот же сценарий через живой ledger, SQL прямо по файлу БД (internal/pipeline/zzprobe_stopmask_test.go): LEDGER[CTRL no-stop] server was asked 3 time(s) LEDGER[CTRL no-stop] spend rows=1 committed=0.00105600 reserved=0.00000000 | checkpoints=1 SUM(cost)=0.00105600 LEDGER[CTRL no-stop] checkpoint attempt=0 finish=connection_lost cost=0.00105600 text_len
- **почему опровергнуть не удалось:** ОПРОВЕРГНУТЬ НЕ УДАЛОСЬ — воспроизвёл своими фикстурами дважды, на транспорте и на живом ledger. Транспорт (мой probe, /tmp/tmp.a93jEjXlLZ/backend/internal/llm/zzrev_probe_test.go): цепочка «вызов1 = 200 + 24 байта + hijack-close (Billable=true, retryable) → вызов2 = 503 → cancel через 300 мс внутри бэкоффа»: PROBE1 STOP-MASK: calls=2 cut survives the stop = false Billable = false err: context canceled КОНТРОЛЬ ВНУТРИ ТЕСТА: calls==2 (t.Fatalf иначе) и errors.Is(err, context.Canceled). КОНТРОЛЬ ВТОРОЙ, ТА ЖЕ ЦЕПОЧКА БЕЗ СТОПА: PROBE1 CONTROL no-stop: calls=3 cut survives = true Billable = true — значит первый вызов действительно строил оплачиваемый обрыв, и теряет его именно выход по отмене.
- **воспроизведение:** `cd /tmp/tmp.a93jEjXlLZ/backend && go test ./internal/llm/ -run 'TestZZProbeStopMask' -count=1 -v 2>&1 | grep PROBE1 && go test ./internal/pipeline/ -run 'TestZZProbeLedgerStopMask' -count=1 -v -timeout 300s 2>&1 | grep -E 'LEDGER|VERDICT'`
### P0 · `money-predicate#2` — Оплаченный обрыв маскируется НЕоплачиваемым обрывом той же цепочки: книжится $0
- **действие:** ЧИНЮ
- **где:** `internal/llm/httpllm.go:249-251 (withEarlierCut)`
- **как:** ОБЪЕДИНЕНО с retry-loop#1. `withEarlierCut` отдаёт финальную ошибку, если она несёт ЛЮБОЙ обрыв. После разведения предикатов это неверно: поздний Billable=false затирает ранний Billable=true. Правило должно быть «предпочесть ОПЛАЧИВАЕМЫЙ обрыв», а не «любой».
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** `withEarlierCut` отдаёт финальную ошибку нетронутой, если она сама несёт какой-нибудь AttemptCutError: «the chain already ends carrying one; the money is not lost». До разведения предикатов это было верно — деньги рисовались на `Delivered`, а он true у обоих обрывов. После разведения второй обрыв может быть Billable=false и молча ЗАТИРАЕТ первый, Billable=true. Цепочка «вызов1: 200 + 24 байта тела + RST (Billable=true) → ретрай: RST до статусной строки (Billable=false)» приезжает к сеттлу как Billable=false, и cutcall.go:90-91 пишет тот же чекпойнт с нулём. Направление — недо-счёт (D39.196 п.2а его терпит), но ряд «after 2xx · connection lost» таблицы девяти заявляет wantPaid:true, и эта форма его нарушает молча; в таблице нет ни одного ряда со СМЕШАННОЙ цепочкой. Хуже того, WARN-строка оператору (cutcall.go:132-135) печатает `after_headers=false bytes_read=0 whitespace_only=true` — то е
- **улика линзы:** Транспортный уровень (internal/llm/zzprobe_mask_test.go), контроль внутри теста — ровно 2 вызова: PROBE MASK: server was asked 2 time(s); the error the caller gets has AfterHeaders=false Billable=false bytes=0 Deliveries=2 Деньги через живой ledger (internal/pipeline/zzprobe_ledger_test.go), A — контроль (оба обрыва после 2xx), B — смешанная цепочка: LEDGER[A both-after-headers] spend rows=1 committed=0.00105600 reserved=0.00000000 | checkpoints=1 SUM(cost)=0.00105600 LEDGER[A both-after-headers] checkpoint attempt=0 finish=connection_lost cost=0.00105600 text_len=0 LEDGER[B masked] spend rows=1 committed=0.00000000 reserved=0.00000000 | checkpoints=1 SUM(cost)=0.00000000 LEDGER[B masked] ch
- **почему опровергнуть не удалось:** ОПРОВЕРГНУТЬ НЕ УДАЛОСЬ — воспроизвёл на транспорте и на ledger. Транспорт (мой probe, internal/llm/zzrev_probe_test.go): «вызов1 = 200 + 24 байта + close (Billable=true) → вызов2 = close до статусной строки»: PROBE2 MASK: calls=2 the caller gets AfterHeaders=false Billable=false bytes=0 Deliveries=2 cause=connection_lost КОНТРОЛЬ: calls==2 через t.Fatalf, и errors.As обязан найти обрыв. Деньги (мой probe, internal/pipeline/zzrev_probe_test.go), A — контроль (оба обрыва после 2xx), B — смешанная цепочка: LEDGER[A both-after-headers] committed=0.00105600 reserved=0.00000000 | checkpoints=1 LEDGER[A both-after-headers] checkpoint attempt=0 finish=connection_lost cost=0.00105600 text_len=0 LEDG
- **воспроизведение:** `cd /tmp/tmp.a93jEjXlLZ/backend && go test ./internal/llm/ -run 'TestZZProbeMaskByLaterUnbillableCut' -count=1 -v 2>&1 | grep PROBE2 && go test ./internal/pipeline/ -run 'TestZZProbeLedgerMask' -count=1 -v -timeout 300s 2>&1 | grep -E 'LEDGER|VERDICT|we cut a delivered'`
### P0 · `retry-loop#1` — withEarlierCut выбрасывает ОПЛАЧЕННЫЙ обрыв первой попытки, если цепочку заканчивает ДРУГОЙ обрыв — движок книжит $0
- **действие:** ЧИНЮ
- **где:** `internal/llm/httpllm.go:248-251`
- **как:** дубль money-predicate#2
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Ветка `var cut *AttemptCutError; if errors.As(final, &cut) { return final // the chain already ends carrying one; the money is not lost }` проверяет ТИП, а не ДЕНЬГИ. Ровно в этом паке деньги развели на второй предикат `Billable = answered && firstByte`, и он `errors.As` не виден. Билейбл-обрыв бывает ТОЛЬКО на пути «2xx пришёл, тело не дочиталось» (`httpllm.go:571`, `answered=true`); обрыв с пути ошибки `http.Client.Do` (`httpllm.go:528`) всегда `answered=false``Billable=false`. Значит порядок двух обрывов в цепочке РЕШАЕТ деньги: билейбл-первый + небилейбл-второй → до `settleCutCall` доезжает только второй, и `cutcall.go:90-93` (`cost := 0.0; if cut.Billable { cost = settleUSDForCutCall(c.estimate) }`) книжит НОЛЬ за генерацию, которую провайдер подтвердил своим 200 и начал отдавать. Достижимо: `complete()` (`httpllm.go:466-469`) при втором доставленном обрыве принудительно ставит `
- **улика линзы:** Проба на httptest (два реальных вызова): попытка 1 — 200 + 21 байт тела + hijack-close (билейбл обрыв), попытка 2 — hijack-close до единого байта ответа (доставлен, не билейбл). $ go test ./internal/llm/ -run 'TestProbe_BillableFirstCutSwallowedByNonBillableSecond' -v calls=2 final err type=*llm.AttemptCutError p: delivered request cut by connection_lost before any response byte after 0s: Post "http://127.0.0.1:36973/chat/completions": EOF errors.As picks: cause=connection_lost delivered=true afterHeaders=false BILLABLE=false deliveries=2 bytes=0 reachable cuts: 1 MONEY LOST: attempt 1 was a billable 2xx cut; the caller settles $0 on this value --- FAIL КОНТРОЛЬ (обратный порядок, тот же фик
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СВОИМ ПРОГОНОМ, И НЕ ТОЛЬКО НА УРОВНЕ llm — ДОВЕДЕНО ДО ЛЕДЖЕРА. Я не поверил транспортной пробе и построил сквозную на боевом харнессе cutcall_test.go (newCutServer + setupCutProject + readMoney). Форма A (попытка 1: 200 + 24 байта тела + hijack-close = Billable; попытка 2: сокет умер до единого байта ответа): server calls=2 committed=0.000000 checkpoints=1 chunk_status_rows=0 estRows=0 checkpoint: stage=draft cost=0.000000 КОНТРОЛЬ — ТОТ ЖЕ фикстур-каркас, события в обратном порядке: CONTROL server calls=2 committed=0.001056 estRows=1 checkpoint cost=0.001056 То есть прибор мерит именно порядок, а не «фикстура не доехала»: два вызова в обоих случаях, 200 с телом в обоих случая
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/tmp.66rCA9lVBc/backend/internal/pipeline/zzprobe_test.go "$d/backend/internal/pipeline/" && cd "$d/backend" && go test ./internal/pipeline/ -run 'TestProbeE2E_BillableFirstCutThenPreHeaderSecond|TestPr`
### P0 · `критик#2``cancelled` читается прогнозом как ПОЛНОСТЬЮ отработанная позиция: одна остановка роняет projected_book_usd на 9.9% — носитель resolvedForResume правлен в двух местах из четырёх
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/rebill.go:279 + status.go:564-573`
- **как:** resolvedForResume подставлен в ДВА носителя из ЧЕТЫРЁХ: прогноз и состояние чанка читают `cancelled` как полностью отработанную позицию ⇒ одна остановка роняет projected_book_usd на 9.9 %. Это число читает платформа.
- **тяжесть после опровержения:** money · **источник:** критик полноты
- **в чём дефект:** Дофикс завёл ОДИН предикат resolvedForResume (cutcall.go:187) — «строка cancelled не ответ, её пере-делают за деньги» — и подставил его в ДВА места: в резюм-гейт (stagerun.go:93) и в объём (volume.go:830, с отдельным комментарием «THE SAME QUESTION runStage ASKS, ASKED THROUGH THE SAME PREDICATE»). Третье и четвёртое место, задающие тот же вопрос, остались нетронутыми: resolveChunkState отдаёт на строку cancelled состояние ChunkFlagged, а projectBookUSD считает ChunkFlagged «полностью отработанной» единицей и берёт её в ЗНАМЕНАТЕЛЬ экстраполяции книги. До дофикса остановка не оставляла строки вовсе (ChunkPending), и знаменатель был честен — то есть прогноз ухудшила именно новая метка. Дальше число не косметическое: projected_book_usd — это и порог согласия на пере-оплату (rebill.go:295: ратифицированный min($0.50, 5% × ProjectedBookUSD)), так что вместе с прогнозом сжимается и порог, за
- **улика линзы:** Проба (10 единиц, истинная цена $0.10 за единицу, репрайсер набит вручную; платных вызовов нет): PROBE CONTROL units=10, per-unit true price=$0.100000 resolveChunkState on a lone `cancelled` row -> State="flagged" Reason="cancelled" (ChunkFlagged="flagged") projected_book_usd ALL TEN DONE = $1.000000 ← контроль сверху projected_book_usd stop leaves NO mark = $1.000000 ← как было ДО дофикса (знаменатель 9) projected_book_usd stop leaves `cancelled` = $0.901056 ← как стало (знаменатель 10) the mark moves the operator's forecast by = $-0.098944 (-9.9%) Код: rebill.go:279 `if res.State != ChunkDone && res.State != ChunkFlagged { continue }`; status.go:572-573 `if res.State != ChunkFlagged { res.
### P1 · `bank-and-counters#2` — Зонд смотрит только на попытку 0: партия, уже ОТВЕЧЕННАЯ на попытке 1, требует полный `want` из суб-бюджета, который никогда не потратит
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/terminologist.go:792`
- **как:** Зонд зашит на попытку 0, а обход ожога идёт по ВОЗРАСТАЮЩИМ индексам: партия, отвеченная на попытке 1, требует полный want из суб-бюджета, который не потратит. Зонд должен спрашивать ту же ось, что и обход.
- **тяжесть после опровержения:** money · **источник:** линза+опровергатель
- **в чём дефект:** Дофикс п.2 сам создаёт форму «ожог на попытке 0 + ответ на попытке 1»: runAttempt перешагивает ожог и отдаёт ответ следующего ключа за $0. Зонд же адресует ТОЛЬКО попытку 0, видит там ожог и отвечает «не оплачено». Планировщик тогда списывает с суб-бюджета полную оценку вызова, которого не будет. На тесном бюджете это отбрасывает партии, суммарная цена которых — ноль.
- **улика линзы:** Проба: партия 0 = ожог(cancelled,"") на попытке 0 + ответ("stop") на попытке 1; партия 1 = обычный ответ. PROBE on a batch already answered at attempt 1: paid=false err=<nil> TIGHT burned@0+answered@1 / answered budget=$0.000001: err=<nil> attempted=0 dropped=2 texts=["" ""] cost=0.000000 provider_calls=0 ROOMY same fixture budget=$100: err=<nil> attempted=2 dropped=0 texts=["термин\tterm" "термин2\tterm2"] cost=0.000000 provider_calls=0 Один и тот же фикстур: реальная цена прохода $0.000000 и НОЛЬ обращений к провайдеру, но тесный бюджет выбрасывает обе партии.
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ. terminologist.go:792 зашивает индекс попытки нулём: `r.attemptRequest(st, st.Model, snapID, ch, 0, maxTokens, msgs)`, тогда как stagerun.go:488-499 (`for { … attempt++ }`) шагает по ВОЗРАСТАЮЩИМ индексам, то есть форму «ожог на 0 + ответ на 1» создаёт сам дофикс п.2. Мой прогон (партия 0: burned@attempt0 + answered@attempt1; партия 1: обычный ответ): PROBE on a batch already answered at attempt 1: paid=false err=<nil> burned@0+answered@1 / answered budget=$0.000001: attempted=0 dropped=2 texts=["" ""] cost=0.000000 provider_calls=0 burned@0+answered@1 / answered budget=$100: attempted=2 dropped=0 texts=["термин\tterm" "термин2\tterm2"] cost=0.000000 provider_calls=0 Один и тот ж
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/pipeline_zz_probe_test.go "$d/backend/internal/pipeline/zz_probe_test.go" && cd "$d/backend" && g`
### P1 · `bank-and-counters#5``Deliveries` считает ОБРЫВЫ, а не доставки: и доккомментарий, и строка леджера утверждают число, которое замеряется неверным
- **действие:** ЧИНЮ
- **где:** `internal/llm/attemptcut.go:104-109 + httpllm.go:459-470 + pipeline/cutcall.go:115-122`
- **как:** Deliveries инкрементируется только под `errors.As(err,&cut)`, то есть считает ОБРЫВЫ, а не доставки: доккомментарий и строка леджера называют неверно замеренное число. Либо считать доставки честно, либо переименовать поле и текст.
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** Поле объявлено как «how many times THIS request reached the provider inside one retry chain», а инкрементируется ровно там, где ошибка попытки ОКАЗАЛАСЬ типизированным обрывом. Любая другая доставка в той же цепочке — оплаченный 2xx с нечитаемым телом (`BilledDecodeError`, httpllm.go:478-486), терминальный 4xx, 5xx — в счёт не идёт. Строка, которую дофикс пишет в `request_log.err` («the provider was asked %d times»), в смешанной цепочке называет число меньше фактического, и молчит целиком при Deliveries==1 — то есть ровно там, где разрыв между спрошенным и записанным больше всего (оплаченный декод + оценка обрыва). Дополнительно: `BilledDecodeError` первой попытки в финальной ошибке не остаётся вовсе.
- **улика линзы:** MIXED CHAIN: provider asked 3 times (1 billed 2xx + 2 cut); errors.As(cut)=true Deliveries=2; billed-decode still in the tree=false final error: p: delivered request cut by connection_lost after 21 body bytes after 1ms: unexpected EOF Фикстур: вызов 1 — 200 с нечитаемым телом (оплачен), вызовы 2-3 — обрыв после заголовков.
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ обе половины. httpllm.go:459-470 инкрементирует `deliveredCutSeen` ровно под `errors.As(err, &cut)`, тогда как attemptcut.go:104-105 объявляет поле как «how many times THIS request reached the provider inside one retry chain». Прогон 1 — смешанная цепочка (вызов 1: 200 с нечитаемым телом, вызовы 2-3: обрыв после заголовков): MIXED CHAIN: provider asked 3 times; errors.As(cut)=true Deliveries=2; billed-decode still in the tree=false final error: p: delivered request cut by connection_lost after 21 body bytes after 1ms: unexpected EOF Прогон 2 — тот же фикстур при max_attempts=2: SILENT-AT-ONE: provider asked 2 times; Deliveries=1; ledger note printed=false То есть строка cutcall.g
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/llm_zz_probe_test.go "$d/backend/internal/llm/zz_probe_test.go" && cd "$d/backend" && go test ./i`
### P1 · `burn-walk#4` — Строка остановленной позиции пишет attempts=0 при реально уплаченных деньгах
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:170-179`
- **как:** ОБЪЕДИНЕНО с cancelled-mark#3. `attemptsMade` присваивать ДО проверки ошибки, иначе метка пишет attempts=0 при уплаченных деньгах.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** Дофикс сознательно поднял накопление денег ВЫШЕ проверки ошибки (`cumCost += att.cumCost` на 171, затем `if err != nil { return }`), чтобы defer с recordCancelledStage видел деньги. Но `attemptsMade = attempt + 1` остался НИЖЕ возврата (179), а `attempt = att.attempt` — тоже ниже (177). Значит на выходе по обрыву defer закрывается над attemptsMade предыдущей итерации, то есть 0 на первом же вызове. Ничего на этом не гейтится (единственные читатели chunk_status.Attempts — resume.go:27 и cmd/tmctl/render.go:112,423), поэтому это телеметрия, а не деньги; но оператор в `tmctl flagged` видит строку «attempts=0, cost_usd=0.002112». Однострочно лечится переносом `attemptsMade = att.attempt + 1` к строке 171-172, рядом с деньгами: att.attempt заполнен обходом и на путях ошибки тоже.
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeWhatTheStoppedRowReports -v: after stop 1: chunk_status attempts=0 cost=0.00105600 flag="cancelled" | checkpoints=1 SUM=0.00105600 (server calls=1) after stop 2: chunk_status attempts=0 cost=0.00211200 flag="cancelled" | checkpoints=2 SUM=0.00211200 (server calls=2) Для сравнения — успешно завершённая строка после трёх сожжённых ключей считает ключи честно: «chunk_status: disposition=ok attempts=4 cost_usd=0.00200000» (TestProbeThreeBurnedKeysInARow).
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СВОИМ ПРОГОНОМ, тяжесть подтверждаю как minor. Механика ровно как описано: в петле runStage `cumCost += att.cumCost` стоит на 171, `if err != nil { return nil, err }` на 172-174, а `attempt = att.attempt` (177) и `attemptsMade = attempt + 1` (179) — НИЖЕ возврата, поэтому defer с recordCancelledStage (148-153) закрывается над значением предыдущей итерации. Мои прогоны: probe A: run1 chunk_status: disp=flagged flag=cancelled attempts=0 cost=0.00105600 (server calls=1) probe D (два стопа подряд): after stop 1: chunk_status attempts=0 cost=0.00105600 flag="cancelled" | checkpoints=1 SUM=0.00105600 | server calls=1 after stop 2: chunk_status attempts=0 cost=0.00211200 flag="cancelle
- **воспроизведение:** `cd /home/ubuntu-26/projects/textmachine/backend && d=$(mktemp -d) && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' . && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/zzprobe_test.go "$d/backend/internal/pipeline/" && (cd "$d/backend" && go test ./internal/pipeline/ -run T`
### P1 · `cancelled-mark#3` — Метка врёт числом попыток: attempts=0 при оплаченном cost_usd=0.001056
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:170-179`
- **как:** дубль burn-walk#4, чинится одной правкой
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** `cumCost += att.cumCost` (171) стоит ДО проверки ошибки (173) — деньги в замыкание попадают. А `attemptsMade = attempt + 1` (179) стоит ПОСЛЕ неё, поэтому обрыв на первой же попытке оставляет `attemptsMade == 0`. Ответ на вопрос пака «нет ли выхода, где счётчики ещё нулевые, а деньги списаны»: есть, ровно один, и это самый частый выход — стоп над первым вызовом стадии. Оператор в `tmctl` (cmd/tmctl/render.go:423 печатает Attempts рядом с CostUSD) видит строку, противоречащую самой себе: «попыток 0, потрачено $0.001056».
- **улика линзы:** go test ./internal/pipeline/ -run TestProbeLoopStopMarkCountsTheAttempt (3 прогона подряд, идентично): CONTROL: server calls=1 committed=0.00105600 checkpoints=1 rows=1 CONTROL row: disp=flagged flag=cancelled attempts=0 cost_usd=0.00105600 THE MARK LIES BY NUMBER: cost_usd=0.00105600 was booked and the row says attempts=0 Код: stagerun.go:171 `cumCost += att.cumCost` / 173 `if err != nil {` / 174 `return nil, err` / 179 `attemptsMade = attempt + 1`.
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО своим прогоном, 3 из 3 идентичны. Код ровно как заявлено: stagerun.go:171 `cumCost += att.cumCost` → :173 `if err != nil` → :174 `return nil, err`, а :179 `attemptsMade = attempt + 1` за проверкой. Замер (одностадийный setupCutProject, early200+300 мс, стоп над ПЕРВЫМ вызовом стадии): CONTROL: server calls=1 committed=0.00105600 checkpoints=1 rows=1 / CONTROL row: disp=flagged flag=cancelled attempts=0 cost_usd=0.00105600. Опровергнуть по «это фикстура» не вышло: деньги приходят из settleCutCall (cutcall.go:110 `att.cumCost, att.runCost = att.cumCost+cost, cost`) и возвращаются ВМЕСТЕ с ошибкой, так что дыра структурная, а не тайминговая. НО тяжесть исходная завышена: единственн
- **воспроизведение:** `# та же копия и тот же скопированный probe-файл: cd "$d/backend" && go test ./internal/pipeline/ -run TestProbeAdvFirstCallStopAttempts -count=1 -v -timeout 120s 2>&1 | grep -E 'CONTROL|--- |THE MARK'`
### P1 · `money-predicate#3` — Половина `answered &&` денежного предиката не закреплена НИЧЕМ: мутант выживает во всей батарее
- **действие:** ЧИНЮ
- **где:** `internal/llm/attemptcut.go:212`
- **как:** Половина `answered &&` денежного предиката не закреплена ничем — мутант переживает всю батарею. Нужна фикстура, где answered=false при firstByte=true (отказ + RST поверх пишущегося тела) И проверка ДЕНЕГ, а не только типа ошибки.
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** `Billable: answered && tr.afterHeaders()`. Слово `Billable` не встречается ни в одном тесте дерева (0 вхождений на 219 тест-файлов); единственное покрытие — косвенное, через ledger в таблице девяти. Обе половины конъюнкции проверены мутацией по одной: `Billable: answered` выживает (вторая половина мертва — на единственном сайте с answered=true afterHeaders всегда true) и `Billable: tr.afterHeaders()` выживает тоже. Вторая мутация — это ровно возврат к той редакции предиката, которую дофикс объявляет неверной и дорогой. Она не ловится, потому что кейс отказа (401+RST) закрыт РАНЬШЕ — там `delivered()` даёт false и обрыв не строится вовсе; а форма, где половина решает («ушёл целиком, первый байт ответа пришёл, но 2xx-объекта нет»), в наборе фикстур отсутствует.
- **улика линзы:** Отсутствие пина, с контрольной величиной: $ grep -rn "Billable" --include=*_test.go . → count: 0 $ find . -name '*_test.go' | wc -l → 219 Мутация посажена по одной, прогон llm-батареи обрывов + таблицы девяти: ### MUTANT Billable = answered -> ok textmachine/backend/internal/llm 8.297s / ok .../internal/pipeline 8.795s ### MUTANT Billable = tr.afterHeaders() -> ok textmachine/backend/internal/llm 8.390s / ok .../internal/pipeline 8.397s (-run 'TestDeliveryIs|TestARefusal|TestARedirect|TestAReplyThatOutruns|TestAFailedWrite|TestADeliveredCut|TestABrokenConnection|TestACancelledRun|TestAStopDuring' и 'TestTheNineOutcomesOfACall') Что именно уезжает — две живые формы провода (internal/llm/zzpro
- **почему опровергнуть не удалось:** ОПРОВЕРГНУТЬ НЕ УДАЛОСЬ, и я расширил замер против исходного: другой ревьюер гонял подмножество -run, я прогнал ОБА ПАКЕТА ЦЕЛИКОМ. Контрольные величины покрытия (греп по копии, мои probe-файлы исключены): grep -rn "Billable" --include=*_test.go . | grep -v zzrev_probe | wc -l → 0 find . -name '*_test.go' | wc -l → 220 (219 родных + мой probe) живые носители: cutcall.go:91, attemptcut.go:103, attemptcut.go:212 — три, из них один читатель Мутация посажена по одной, sed по attemptcut.go:212, оригинал восстановлен после: ### MUTANT A: Billable = answered → ok internal/llm 9.370s / ok internal/pipeline 37.720s (ПОЛНЫЕ ПАКЕТЫ, без -run) ### MUTANT B: Billable = tr.afterHeaders() → ok internal/llm
- **воспроизведение:** `cd /tmp/tmp.a93jEjXlLZ/backend && cp internal/llm/attemptcut.go /tmp/ac.orig && sed -i 's/Billable: answered && tr.afterHeaders(),/Billable: tr.afterHeaders(),/' internal/llm/attemptcut.go && go test ./internal/llm/ ./internal/pipeline/ -count=1 -timeout 900s; go test ./internal/llm/ -run TestZZProbeHalfPin -count=1 -v | grep PROBE; cp /tmp/ac.orig internal/llm/attemptcut.go`
### P1 · `transport-and-config#1` — Пин write-бонда проверяет НЕ тот транспорт, который уходит в бой: удаление тюнинга из боевого клиента переживает всю батарею пакета
- **действие:** ЧИНЮ
- **где:** `internal/llm/attemptcut_test.go:853 против httpllm.go:124`
- **как:** Пин write-бонда конфигурирует СВОЙ транспорт, а не тот, что уходит в бой: удаление тюнинга из боевого клиента переживает всю батарею. Пин обязан читать транспорт, собранный keepAliveHTTPClient.
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** Тест берёт СВЕЖИЙ клон (`h2 := tuneHTTP2(http.DefaultTransport.(*http.Transport).Clone())`) и утверждает про него, а не про транспорт, который построил keepAliveHTTPClient. Значит `tuneHTTP2(base)` можно вынуть из боевого конструктора целиком — облачный клиент останется БЕЗ write-бонда, БЕЗ ReadIdleTimeout и БЕЗ PingTimeout — и ни один тест этого не заметит. Это ровно тот дефект, который комментарий tuneHTTP2 объявляет починенным («пин ПЕРЕЖИЛ свою мутацию»), сдвинутый на один шаг наружу: раньше не проверялось значение, теперь не проверяется установка.
- **улика линзы:** Мутация в копии: строка 124 заменена на `// MUTANT: tuneHTTP2(base)`. $ go test ./internal/llm/ -run TestTheWriteSideKeepaliveIsDerivedFromTheReadSideOne -v → `--- PASS (0.00s)`. $ go test ./internal/llm/ -count=1 → `ok textmachine/backend/internal/llm 9.276s` (контроль: в пакете 44 функции Test — `grep -c '^func Test' internal/llm/*_test.go` = 44; мой пробник на время прогона был вынесен из дерева). Что мутант ловится функционально: тот же пир, тот же клиент — чистое дерево обрывает вызов на 20.03 с («write tcp …: i/o timeout»), мутант держит 40.03 с («write: connection reset by peer», то есть пир сам сдался). Восстановление проверено md5: httpllm.go в копии == оригинал.
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО своим прогоном. attemptcut_test.go:853 читает `tuneHTTP2(http.DefaultTransport.(*http.Transport).Clone())` — свежий клон, а не транспорт из keepAliveHTTPClient (httpllm.go:122-126). Мутация в КОПИИ: строка 124 заменена на `// MUTANT: tuneHTTP2(base)``go test ./internal/llm/ -count=1` дал `ok textmachine/backend/internal/llm 9.353s`, то есть мутант ПЕРЕЖИЛ весь пакет. Контроль числом: в пакете 44 функции Test (`grep -h '^func Test' internal/llm/*_test.go | wc -l` = 44), 0 FAIL. Носителей больше нет: `grep -rn 'keepAliveHTTPClient|tuneHTTP2|WriteByteTimeout|ReadIdleTimeout' --include=*.go .` даёт 23 строки, все в httpllm.go, provider_local.go (комментарий) и attemptcut_test.go
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cd "$d/backend" && sed -i '124s|.*|\t// MUTANT: tuneHTTP2(base)|' internal/llm/httpllm.go && go test ./internal/llm/ -count=1`
### P1 · `transport-and-config#2` — Комментарий обещает закрыть awaitFlowControl-парковку — замер показывает, что она открыта: 3-секундный дедлайн держался >70 с на БОЕВОМ клиенте
- **действие:** ЧИНЮ КОММЕНТАРИЙ, механизм — ПИНГ
- **где:** `internal/llm/httpllm.go:97-111`
- **как:** Комментарий обещает, что бонд закрывает awaitFlowControl-парковку. ЗАМЕР ОПРОВЕРГАЕТ: 3-секундный дедлайн держался >70 с на боевом клиенте. Комментарий — мой и врёт, его правлю. Оставлять ли сам бонд — решение оркестратора (см. пинги ниже).
- **тяжесть после опровержения:** correctness · **источник:** линза+опровергатель
- **в чём дефект:** Комментарий: «A peer that answers an early 2xx and then stops READING parks the write goroutine in awaitFlowControl on a sync.Cond, which a context cannot wake … ⛔ WITHOUT IT OUR OWN DEADLINE IS INERT ON h2». WriteByteTimeout бьёт ТОЛЬКО по заблокированной записи в СОКЕТ (x/net/http2/http2.go:325 writeWithByteTimeout ← transport.go:502 stickyErrWriter). Парковка в awaitFlowControl (x/net/http2/transport.go:1661 `cc.cond.Wait()`) — другой затык: сокет писать не пытаются, дедлайн сокета не взведён, будить некому. Пир, который ЧИТАЕТ сокет и отвечает на PING, но не пополняет спековое окно 65535, вешает вызов бессрочно и при включённом бонде.
- **улика линзы:** Пробник TestZZFlowControlPark (сырой h2-пир: SETTINGS INITIAL_WINDOW_SIZE=65535, ни одного WINDOW_UPDATE, PING-и ацкает), боевой клиент keepAliveHTTPClient(), тело 4 MiB, наш дедлайн 3 с: `OUR deadline was 3s; STILL HANGING after 70s` peer log: `[0.01s] ALPN="h2"` / `[0.01s] request headers on stream 1 -> answering 200` / `[15.02s] PING -> ack` / `[30.03s] PING -> ack` / `[45.04s] PING -> ack` / `[60.05s] PING -> ack` — читающая сторона жива, keepalive не срабатывает, write-бонд не срабатывает.
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО и УСИЛЕНО. Прогон zzflow_test.go на БОЕВОМ клиенте (keepAliveHTTPClient, тронут только TLS-trust) против h2-пира, который отвечает 200 и не потребляет тело: `PRODUCTION client, our deadline was 3s: STILL HANGING after 45.01s (first byte seen=true)`. Стек из паники прогона называет место дословно: `golang.org/x/net/http2.(*clientStream).awaitFlowControl(...) transport.go:1661``writeRequestBody transport.go:1558``doRequest`. Механика подтверждена чтением: transport.go:1661 — `cc.cond.Wait()`, а разбудить его может только Broadcast; после того как roundTrip вернул 200 (handleResponseHeaders, transport.go:979-1003), на ctx.Done не смотрит НИКТО, и WriteByteTimeout сюда не дост
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/zzflow_test.go /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d037`
### P1 · `transport-and-config#3` — Пол скорости zai — данные без носителя: каталожный гейт zai вообще не смотрит, удаление `tok_s_floor: 35` проходит все 78 кейсов internal/config
- **действие:** ЧИНЮ
- **где:** `internal/config/models_catalog_test.go:219-225`
- **как:** Гейт смотрит на провайдеров через MinMaxTokens и `if grant == 0 { continue }`, поэтому zai не смотрит вовсе: удаление `tok_s_floor: 35` проходит все 78 кейсов. Расширить гейт на объявленные поля, а не только на выведенные гранты.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** Гейт берёт «самый большой бюджет провайдера» как MinMaxTokens(модель)*2. Обе модели zai (glm-5, glm-5.1) объявляют min_max_tokens = 0, поэтому biggest["zai"] == 0 и провайдер молча пропускается. Гейт, которым обосновано «каталог обязан сказать одно из двух», о zai не спрашивает ничего, а других носителей у числа 35 нет.
- **улика линзы:** $ go test ./internal/config/ -run TestAProviderThatWillWaitLongSaysSoInTheCatalog -v → `providers in the catalogue: 8; with a declared token floor: 3; whose biggest call outgrows its own attempt_s …: 3; missing both: 0` — проверенные трое это deepseek/kimi/gemini (пробник печатает min_max_tokens: deepseek-v4-pro=16000, kimi-k2.6=16000, gemini=8000, glm-5=0, glm-5.1=0). Мутация: из строки 90 configs/models.yaml убрано `, tok_s_floor: 35`. $ go test ./internal/config/ → `ok … 0.060s`; контроль числом: `go test -v | grep -c '^--- PASS'` = 78. Файл восстановлен (md5 совпадает с оригиналом).
- **почему опровергнуть не удалось:** ФАКТ ПОДТВЕРЖДЁН, ТЯЖЕСТЬ СНИЖЕНА. Гейт (models_catalog_test.go:216-224) строит `biggest[mod.Provider] = MinMaxTokens*2`, а `if grant == 0 { continue }` выкидывает провайдера, у которого ни одна модель не объявила min_max_tokens. Мой прогон печатает `providers in the catalogue: 8; with a declared token floor: 3; whose biggest call outgrows its own attempt_s: 3; missing both: 0`. Мой пробник по боевому каталогу: `zai model glm-5 tok_s_floor=35 min_max_tokens=0`, `zai model glm-5.1 tok_s_floor=35 min_max_tokens=0` (контроль: zai-моделей 2, всего моделей 10, провайдеров 8) — то есть biggest["zai"]==0 и провайдер молча пропущен. Мутация в копии (`sed -i '90s/, tok_s_floor: 35//' configs/models.y
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cd "$d/backend" && go test ./internal/config/ -count=1 -v | grep -c '^--- PASS' && sed -i '90s/, tok_s_floor: 35//' configs/models.yaml && go test ./internal/config/ -count=1 -v | grep -cE '^--- (PASS|FAIL)'`
### P1 · `transport-and-config#4`У zai объявлен пол без потолка, и связку никто не валидирует: опечатка в поле даёт 2 ч 32 мин на вызов, а потолок ниже пола обнуляет заявленное «attempt_s — это ПОЛ» до 10 с
- **действие:** ЧИНЮ
- **где:** `internal/config/models.go:122-146 + internal/llm/attemptcut.go:281-289`
- **как:** ОБЪЕДИНЕНО с критик#1. Ни одной проверки диапазона: опечатка в поле даёт 2 ч 32 мин на вызов, а attempt_max_s НИЖЕ attempt_s молча укорачивает каждый вызов — прямо вопреки моему же комментарию «attempt_s is the FLOOR … no provider loses a second it has today». Добавить валидацию в LoadModels и пин.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** DeadlineFor сначала поднимает до AttemptTimeout, а потом БЕЗУСЛОВНО срезает до AttemptMax. Конфиг с attempt_max_s меньше attempt_s молча превращает документированный пол в потолок, и каждый вызов режется по нему; загрузчик (строгий по НЕИЗВЕСТНЫМ ключам) про несогласованные ЗНАЧЕНИЯ не говорит ничего. Обратная сторона той же дыры: у zai потолка нет вовсе, поэтому десятичная описка 35 → 3.5 не ограничена ничем. Цена первой ошибки денежная: короткий дедлайн рвёт доставленный вызов, а новый разбор обрыва как раз и записывает такой обрыв оценкой и ретраит его.
- **улика линзы:** Пробник TestZZTimeoutsCoherence (LoadModels на временном yaml, ни один вариант не отвергнут): `shipped-zai-shape loaded OK d(512)=4m0s d(16000)=7m37.142857142s d(32000)=15m14.285714285s` `ceiling BELOW the floor loaded OK d(512)=10s d(16000)=10s d(32000)=10s` `decimal slip 35 -> 3.5 loaded OK d(512)=4m0s d(16000)=1h16m11.428571428s d(32000)=2h32m22.857142857s` `negative floor loaded OK d(512)=4m0s d(16000)=7m30s d(32000)=15m0s` `absurd floor loaded OK d(512)=4m0s d(16000)=4m0s d(32000)=4m0s` Ратифицированный владельцем предел ожидания ~20 мин (им обоснованы attempt_max_s: 1200 у kimi и gemini) в строке zai не выражен ничем.
- **почему опровергнуть не удалось:** МЕХАНИКА ПОДТВЕРЖДЕНА, ТЯЖЕСТЬ СНИЖЕНА С money/high ДО minor. LoadModels (models.go:213-300) валидирует kind, base_url, reasoning, cache_ttl, accepts_labels, capabilities — по ЗНАЧЕНИЯМ Timeouts ни одной проверки; Timeouts участвует только в Profile() (models.go:147-156). Мой пробник построил 6 вариантов, LoadModels отверг 0: `shipped-zai-shape loaded OK d(512)=4m0s d(16000)=7m37.142857142s d(32000)=15m14.285714285s`; `ceiling BELOW the floor loaded OK d(512)=10s d(16000)=10s d(32000)=10s` (attempt_max_s:10 при attempt_s:240 — документированный ПОЛ схлопнут в потолок); `decimal slip 35 -> 3.5 loaded OK d(32000)=2h32m22.857142857s`; `negative floor loaded OK`; `absurd floor loaded OK`; `negat
- **воспроизведение:** `# пробник TestZZTimeoutsCoherence из отчёта; воспроизводится так: d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cd "$d/backend" && grep -n 'Timeouts' internal/config/models.go && sed -n '281,289p' internal/llm/attemptcut.go`
### P1 · `критик#1` — attempt_max_s ниже attempt_s молча УКОРАЧИВАЕТ дедлайн каждого вызова — вопреки комментарию, который на этом обещании и стоит; ни валидации, ни теста, ни пина
- **действие:** ЧИНЮ
- **где:** `internal/config/models.go:122-146 + internal/llm/attemptcut.go:281-289`
- **как:** ДУБЛЬ transport-and-config#4, чинится той же правкой: валидация связки tok_s_floor/attempt_s/attempt_max_s в LoadModels плюс пин на то, что attempt_max_s ниже attempt_s не проходит загрузку.
- **тяжесть после опровержения:** money · **источник:** критик полноты
- **в чём дефект:** Комментарий над DeadlineFor: «attempt_s is the FLOOR, not the value. That is what makes this change safe to land on every existing config at once: a call whose derived time is shorter than the configured deadline keeps the configured one, so NO PROVIDER LOSES A SECOND IT HAS TODAY». Утверждение ложно, как только объявлен attempt_max_s меньше attempt_s: пол применяется ПЕРВЫМ, потолок ВТОРЫМ, потолок побеждает. Ни одной проверки нет: в internal/config 27 вызовов bad("…") и НИ ОДНОГО про timeouts — грепом Timeouts/AttemptS/TokSFloor/QueueSlackS/AttemptMaxS по internal/config (не-тестовые) выдал только объявление структуры и Profile(); валидатора полей таймаутов не существует. Живым это делает НОВЫЙ каталожный гейт: его текст ошибки прямо велит оператору «declare timeouts.tok_s_floor … OR timeouts.attempt_max_s», то есть подталкивает вписать поле, у которого нет нижней границы. Цена именно
- **улика линзы:** Проба (чистая функция, платных вызовов нет), профиль attempt_s=240s / tok_s_floor=35 — реальные числа zai из configs/models.yaml: PROBE CONTROL uncapped deadline for the draft grant 8500 = 4m2.857142857s (floor attempt_s = 4m0s) attempt_max_s=2m0s -> DeadlineFor(8500)=2m0s ; below the configured attempt_s? true attempt_max_s=1s -> DeadlineFor(8500)=1s ; below the configured attempt_s? true attempt_max_s=1ns -> DeadlineFor(8500)=1ns ; below the configured attempt_s? true CONTROL attempt_max_s=20m -> 4m2.857142857s (>= floor: true) ← контроль: прибор спрашивал живой предмет МУТАЦИЯ (посажена, выжила): поменял порядок клампов в DeadlineFor на обратный (потолок первым, пол последним — то есть на
### P1 · `критик#5` — FlagConnectionLost объявлен, отранжирован и НЕДОСТИЖИМ: обоснование в его комментарии не реализовано ни одной строкой кода
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/disposition.go:98-104 + status.go:432`
- **как:** FlagConnectionLost объявлен, отранжирован и НЕДОСТИЖИМ: обоснование в его комментарии не реализовано ни одной строкой. Либо сделать достижимым, либо снять константу и ранг — но не оставлять словами.
- **тяжесть после опровержения:** correctness · **источник:** критик полноты
- **в чём дефект:** Комментарий: «FlagConnectionLost … exists so that a checkpoint recording MONEY WITHOUT A RESULT can never be read as a healthy answer. The live path does not raise it … but a REPLAY OF THAT CHECKPOINT HAS TO SAY SOMETHING, and the zero classification says «ok»». Реплея, который бы это сказал, нет: в classify (disposition.go:305-325) есть ветви decodeErrorFinish и attemptTimeoutFinish, ветви connectionLostFinish НЕТ; во всём не-тестовом коде 16 конструкций `classification{…}` и ни одной с этим флагом; за пределами объявления и строки ранга константа не встречается нигде. Реальную защиту даёт не она, а burnedByCut (disposition.go:207), который такой чекпойнт СЖИГАЕТ — то есть до реплея он не доживает никогда. Утверждение в комментарии нового кода, не проверенное исполнением, ровно того сорта, который просили искать: оно объясняет существование сущности механизмом, которого нет. Как провери
- **улика линзы:** Греп по всему дереву (не-тест + тест): FlagConnectionLost встречается 2 раза — disposition.go:104 и status.go:432. Ни в одном тесте, ни в одной конструкции classification. Греп `classification{Flag` в не-тестовом коде: 16 конструкций, перечислены поимённо — cutcall.go:140, chunkrun.go:86/89/107, disposition.go:307/323/326/328/335/348/359/366/373/375, stagerun.go:714. FlagConnectionLost нет ни в одной. МУТАЦИЯ (посажена, выжила): status.go:432 `FlagConnectionLost: 4``FlagConnectionLost: 0`, то есть «разорванное соединение» становится САМЫМ тяжёлым флагом книги, вровень с жёстким отказом. `go test ./internal/pipeline/ ./cmd/tmctl/` → ok textmachine/backend/internal/pipeline 37.147s ; ok tex
### P1 · `критик#6` — Вся новая таблица тяжести флагов пинуется только на ЧЛЕНСТВО, а не на ЗНАЧЕНИЕ: cancelled можно объявить худшей бедой книги, и батарея пакета зелёная
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/status.go:430-449, пин flagseverity_test.go`
- **как:** Таблица тяжести пинуется на ЧЛЕНСТВО, а не на ЗНАЧЕНИЕ: cancelled можно объявить худшей бедой книги, и батарея зелёная. Нужен пин на ПОРЯДОК (кто кого обязан перевешивать), как у off_target_lang.
- **тяжесть после опровержения:** correctness · **источник:** критик полноты
- **в чём дефект:** Дофикс переписал порядок тяжести и подробно обосновал каждый ранг (decode_error и attempt_timeout вровень; connection_lost туда же; cancelled — «the mildest mark there is», ниже авто-очистки; unknown сдвинут 8→9). Единственный гейт над этой таблицей — TestEveryFlagReasonIsRanked — проверяет, что каждая ОБЪЯВЛЕННАЯ константа ПРИСУТСТВУЕТ в карте; порядок он не проверяет. Единственная новая мутация на этот предмет (CUTFLAG-a-cut-chunk-is-the-mildest-thing-that-can-happen) тоже бьёт по членству — её edit УДАЛЯЕТ строку FlagAttemptTimeout целиком. Значит вся содержательная часть правки (какой флаг тяжелее какого) не пинована ничем, и решение, которое пак объявляет ключевым для паспорта главы, любая следующая смена может переставить бесшумно. Отдельно: обратной проверки — «каждый отранжированный флаг движок умеет выдать» — тоже нет, и именно поэтому мёртвый FlagConnectionLost из находки №5 пр
- **улика линзы:** МУТАЦИЯ (посажена, выжила): status.go:449 `FlagCancelled: 8``FlagCancelled: 0` — остановленная позиция становится ХУДШЕЙ проблемой главы, обгоняя жёсткий отказ, то есть ровно то, что комментарий на :443-448 объявляет недопустимым. `go test ./internal/pipeline/` → ok textmachine/backend/internal/pipeline 34.853s. Ни один тест не прочёл значение. Файл восстановлен, cmp: RESTORED-OK. Тело объявленной мутации (cmd/tmmutate/mutations.json), доказывающее, что пин целит в членство, а не в значение: edits[0] = {"find": "\tFlagDecodeError: 4,\n\tFlagAttemptTimeout: 4,\n", "replace": "\tFlagDecodeError: 4,\n"} — строка удаляется, и её ловит проверка «объявлен, но не отранжирован».
### P2 · `bank-and-counters#6` — Строка «asked N times» невидима там, где колонку читают: errTail режет на 120 байтах, и приписка стоит в отрезаемом хвосте
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/cutcall.go:122 против cmd/tmctl/render.go:512-528`
- **как:** ОБЪЕДИНЕНО с retry-loop#5. Приписка «asked N times» стоит в КОНЦЕ err, а errTail режет на 120 байтах ⇒ до оператора не доходит никогда. Плюс перевод строки от errors.Join печатает одну строку отчёта двумя. Ставить приписку в НАЧАЛО и убирать перевод строки.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** Ответ на п.3 по существу: колонку `request_log.err` НИКТО не парсит — один SELECT (store/requestlog.go:133) и один производственный потребитель (cmd/tmctl/main.go:348 → renderReport → errTail), так что дописанный текст ничего не ломает. Но единственный человеческий читатель обрезает пару degraded+err на 120 байтах, а приписка длиной 73 байта приклеена в самый конец: из неё в ячейку попадает 5 байт, и число обращений — то, ради чего строка написана, — не печатается никогда для реалистичной длины сообщения обрыва.
- **улика линзы:** len(degraded)=15 len(err)=172 len(degraded+' '+err)=188 cap=120 note len=73 ; note bytes surviving in the rendered cell=5 RENDERED CELL: "connection_lost deepseek: delivered request cut by connection_lost after 21 body bytes after 1.003s: unexpected EOF [the…" CONTROL (no note): "connection_lost deepseek: delivered request cut by connection_lost after 21 body bytes after 1.003s: unexpected EOF" Контроль без приписки в 120 байт умещается целиком — значит режет именно приписка, а не базовое сообщение.
- **почему опровергнуть не удалось:** ВОСПРОИЗВЕЛ. cutcall.go:122 приклеивает приписку В КОНЕЦ (`fmt.Sprintf("%s [the provider was asked %d times; …]", err.Error(), cut.Deliveries)`), а cmd/tmctl/render.go:512-528 режет пару degraded+err на 120 байтах. Прогон на реальной форме сообщения: len(degraded)=15 len(base)=99 len(base+note)=172 cap=120 note len=73 RENDERED CELL : "connection_lost deepseek: delivered request cut by connection_lost after 21 body bytes after 1.003s: unexpected EOF [the…" CONTROL(nonote): "connection_lost deepseek: delivered request cut by connection_lost after 21 body bytes after 1.003s: unexpected EOF" note bytes surviving in the rendered cell = 5 of 73 Контроль без приписки умещается в 120 целиком — режет
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/tmctl_zz_probe_test.go "$d/backend/cmd/tmctl/zz_probe_test.go" && cd "$d/backend" && go test ./cm`
### P2 · `cancelled-mark#5` — Комментарий у defer называет носителем банковские батчи, которые в runStage не входят
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:143-144`
- **как:** Комментарий у defer называет носителем банковские батчи, которые в runStage НЕ входят. Убрать их из перечня — иначе комментарий врёт о знаменателе дверей.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** «...the hop, the repair sub-step and the bank batches all leave this function through OTHER returns». Хоп и ремонт — правда (строки 237 и 283). Банковские батчи через `runStage` не проходят ВООБЩЕ: `runBankRoleBatches` вызывается напрямую из terminologist.go:465 и :660, поэтому этот `defer` их не покрывает и покрыть не может. Читающий через полгода заключит, что стоп над банковским батчем помечен, — он не помечен.
- **улика линзы:** grep -rn "runBankRoleBatches" --include=*.go internal/ | grep -v _test → internal/pipeline/terminologist.go:465: run, rerr := r.runBankRoleBatches(ctx, snapID, plan, batches, "render") internal/pipeline/terminologist.go:660: run, err = r.runBankRoleBatches(ctx, snapID, plan, batches, "classify") internal/pipeline/terminologist.go:897: func (r *Runner) runBankRoleBatches(...) Вызывающих runStage в живом коде: 1 (internal/pipeline/waverun.go:542).
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО грепом с контрольными величинами. `grep -rn 'runBankRoleBatches' --include=*.go .` без тестов даёт 4 строки: terminologist.go:465 и :660 — два ВЫЗОВА, :897 — определение, repair.go:252 — упоминание в комментарии; вызовов из stagerun.go — НОЛЬ. Живых вызывающих runStage — ровно 1 (waverun.go:542), и в теле runStage слова о батчах нет (греп по телу :60-470 даёт только banknote/bankFlags — это другой предмет). Значит defer на stagerun.go:150 банковские батчи не покрывает и покрыть не может, а комментарий :143-144 («the hop, the repair sub-step and the bank batches all leave this function through OTHER returns») называет их носителем. Проверил соседний носитель, чтобы не поймать авт
- **воспроизведение:** `cd /home/ubuntu-26/projects/textmachine/backend && grep -rn 'runBankRoleBatches' --include=*.go . | grep -v _test && echo '--- callers of runStage:' && grep -rn 'runStage(' --include=*.go internal/ cmd/ | grep -v 'func (r \*Runner) runStage' && sed -n '142,155p;468,478p' internal/pipeline/stagerun.go`
### P2 · `money-predicate#5` — Лог сожжённого ключа называет «оплаченным» чекпойнт на $0
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/stagerun.go:496-498`
- **как:** Строка «attempt was paid for» печатается над cost_usd=0.000000. Переформулировать: «ключ потрачен» вместо «оплачен», и печатать сумму как есть.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** `burnedByCut` (disposition.go:203-208) стоимость не смотрит вовсе — только пустой текст и finish_reason cancelled/connection_lost. После разведения такой чекпойнт бывает нулевым, и прогулка по сожжённым ключам печатает оператору «attempt was paid for and cut off before its answer arrived» с `cost_usd=0.000000`. Поведение верное (ключ действительно потрачен и ответом служить не может), лжёт только фраза — а это единственная строка, по которой оператор восстанавливает, за что списано.
- **улика линзы:** stagerun.go:496 `r.Log.InfoContext(ctx, "attempt was paid for and cut off before its answer arrived; re-asking under a fresh key at the SAME budget", ... "cost_usd", fmt.Sprintf("%.6f", cp.CostUSD))`; disposition.go:203-208 `func burnedByCut(cp *store.Checkpoint) bool { if cp.ResponseText != "" { return false }; return cp.FinishReason == cancelledFinish || cp.FinishReason == connectionLostFinish }` — cost_usd в предикате отсутствует, а сценарий B выше как раз оставляет такой чекпойнт с cost=0.00000000 и finish=connection_lost.
- **почему опровергнуть не удалось:** ОПРОВЕРГНУТЬ НЕ УДАЛОСЬ — снял строку дословно с прогона, а не с чтения кода. Сценарий: обычный ряд таблицы «before headers · connection lost» (сокет рвётся до статусной строки, два вызова, wantPaid:false), затем резюме на здоровом провайдере (мой probe TestZZProbeBurnedKeyLog, логгер подменён на захватывающий handler): LEDGER[BURN first run] server was asked 2 time(s) ... committed=0.00000000 | checkpoints=1 LEDGER[BURN first run] checkpoint attempt=0 finish=connection_lost cost=0.00000000 text_len=0 BURN predicate: finish=connection_lost cost=0.00000000 text_len=0 -> burnedByCut=true BURN LOG LINE: attempt was paid for and cut off before its answer arrived; re-asking under a fresh key at t
- **воспроизведение:** `cd /tmp/tmp.a93jEjXlLZ/backend && go test ./internal/pipeline/ -run 'TestZZProbeBurnedKeyLog' -count=1 -v -timeout 300s 2>&1 | grep -E 'LEDGER|BURN'`
### P2 · `money-predicate#6` — AfterHeaders=true для 1xx и для незакрытого блока заголовков — оператору сообщают об ответе, которого не было
- **действие:** ЧИНЮ
- **где:** `internal/llm/attemptcut.go:79-80, 211`
- **как:** AfterHeaders=true для 1xx и для незакрытого блока заголовков — оператору сообщают об ответе, которого не было. Либо считать только 2xx-объект, либо переименовать поле в лог-строке.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** `AfterHeaders: tr.afterHeaders()` берётся из GotFirstResponseByte, а тот в net/http срабатывает на ПЕРВЫЙ байт любого ответа, включая информационный 1xx (transport.go:2510 — Peek(1) до разбора статусной строки). Поле объявлено как «a response had begun to arrive — the early-200 case», но становится истинным и там, где 2xx не было и не будет. Денег не двигает (за это отвечает `answered`), однако едет в WARN оператору как `after_headers=true bytes_read=0 whitespace_only=true` — набор, читающийся как «провайдер начал отвечать», при том что не было ни одного байта тела. Читателей поля ровно два и оба — печать (cutcall.go:133 и Error() на attemptcut.go:120), так что цена ошибки — только диагностика.
- **улика линзы:** PROBE 1xx (103) · then our deadline cut=YES cause=attempt_timeout Delivered=true AfterHeaders=true Billable=false Deliveries=1 bytes=0 (connections accepted=1) PROBE HALF-PIN [200 status line, header block never closed]: Delivered=true AfterHeaders=true Billable=false Потребители поля, греп по живому дереву без тестов: `grep -rn "AfterHeaders" --include=*.go . | grep -v _test.go | grep -v attemptcut.go` → 1 строка, cutcall.go:133 (логирование).
- **почему опровергнуть не удалось:** ОПРОВЕРГНУТЬ НЕ УДАЛОСЬ, но тяжесть снижаю с medium до minor. Замер (мой probe internal/llm/zzrev_probe2_test.go, сырой сокет, отвечает после request line и держит молчание, так что вызов кончает НАШ дедлайн; контроль — число принятых соединений печатается): PROBE HALF-PIN [200 status line, header block never closed]: Delivered=true AfterHeaders=true Billable=false cause=attempt_timeout bytes=0 (connections accepted=1) PROBE HALF-PIN [103 Early Hints, then silence]: Delivered=true AfterHeaders=true Billable=false cause=attempt_timeout bytes=0 (connections accepted=1) В обоих случаях 2xx-объекта в руках нет и не будет, а поле, названное «AfterHeaders» и документированное как «a response had b
- **воспроизведение:** `cd /tmp/tmp.a93jEjXlLZ/backend && go test ./internal/llm/ -run 'TestZZProbeHalfPinLiveWireForms' -count=1 -v 2>&1 | grep -E 'PROBE|ok ' && grep -rn "AfterHeaders" --include=*.go . | grep -v _test.go | grep -v attemptcut.go`
### P2 · `retry-loop#4` — Строка операторского журнала утверждает «одна оценка забукана на все», когда забукан НОЛЬ
- **действие:** ЧИНЮ
- **где:** `internal/pipeline/cutcall.go:115-122`
- **как:** Строка утверждает «ONE estimate is booked for all of them», когда забукан НОЛЬ (Billable=false). Условие текста должно смотреть на фактическую сумму.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** `if cut.Deliveries > 1 { rl.Err = fmt.Sprintf("%s [the provider was asked %d times; ONE estimate is booked for all of them]", ...) }` не смотрит на `cut.Billable`, а `cost` тремя строками выше зануляется именно по `Billable`. Достижимое состояние `Deliveries=2 && Billable=false` даёт строку, которая прямо лжёт: оценок забукано ноль, а не одна. Рядом `rl.Estimated = cost > 0` = false, то есть строка ещё и не попадает в легенду оценочных строк — оператор читает её как настоящий $0. Запрет «флаг, врущий о причине» (D39.93 п.2) тут нарушен текстом, а не флагом.
- **улика линзы:** Состояние воспроизведено пробой 1 выше: `errors.As picks: ... BILLABLE=false deliveries=2`. Цитата кода, ветвление без guard-а по Billable: cutcall.go:90-93 cost := 0.0 if cut.Billable { cost = settleUSDForCutCall(c.estimate) } cutcall.go:115 if cut.Deliveries > 1 { cutcall.go:122 rl.Err = fmt.Sprintf("%s [the provider was asked %d times; ONE estimate is booked for all of them]", err.Error(), cut.Deliveries) cutcall.go:128 rl.Estimated, rl.EstTokens = cost > 0, r.estOutTokens(c.chunk.Text)
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО СКВОЗНЫМ ПРОГОНОМ, но тяжесть я снижаю: это ложь В ТЕКСТЕ, не в деньгах. Состояние воспроизведено НЕЗАВИСИМО от находки 1 — две ДО-заголовочные обрыва подряд, ни одна попытка не была billable: calls=2 committed=0.000000 estRows=0 request_log[0]: cost=0.000000 estimated=0 finish=connection_lost err="fake: delivered request cut by connection_lost before any response byte after 1ms: Post …: EOF [the provider was asked 2 times; ONE estimate is booked for all of them]" Забукано ноль оценок; строка говорит «ONE estimate is booked for all of them». Ветвление действительно без гарда: cutcall.go:90-93 зануляет cost по `Billable`, cutcall.go:115 ветвится по `Deliveries > 1`, cutcall.go:12
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/tmp.66rCA9lVBc/backend/internal/pipeline/zzprobe_test.go "$d/backend/internal/pipeline/" && cd "$d/backend" && go test ./internal/pipeline/ -run 'TestProbeE2E_TwoPreHeaderCutsOperatorRow' -v`
### P2 · `retry-loop#5` — errors.Join кладёт перевод строки в request_log.err: одна строка отчёта печатается ДВУМЯ, а заметка о числе доставок обрезается и до оператора не доходит никогда
- **действие:** ЧИНЮ
- **где:** `cmd/tmctl/render.go:512-528`
- **как:** дубль bank-and-counters#6
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** `errors.Join` в `internal/llm` — новьё этого дофикса (0 вхождений в HEAD → 2 в рабочем дереве), а его `Error()` разделяет слагаемые `\n`. Текст едет в `rl.Err` (`cutcall.go:114`, `:122`) → колонка `request_log.err` → единственный потребитель `tmctl report` (`main.go:348``render.go:385``errTail`). `errTail` режет по 120 БАЙТАМ и НЕ трогает переводы строк, а таблица печатается фиксированными колонками `%-20s %-4s …`. Следствие два: (а) одна запись отчёта рвётся на две строки, вторая идёт без колонок и читается как битая запись; (б) маркер `[the provider was asked N times; …]` приклеен ПОСЛЕ `err.Error()` и обрезкой сносится — причём и без join-а тоже, то есть единственная читаемая формулировка недосчёта, которую пак добавил, в отчёт не попадает НИКОГДА.
- **улика линзы:** $ go test ./cmd/tmctl/ -run 'TestProbe_JoinedErrorInTheOperatorTable' -v raw joined err (143 bytes, 2 lines): p http 400: {"error":{"message":"model not found"}} p: delivered request cut by connection_lost after 21 body bytes after 900ms: unexpected EOF errTail output: newlines=1 bytes=123 does the deliveries mark survive errTail: false table line 1: "2026-09-08T10:00:00Z 1 4 draft translator deepseek-chat … connection_lost p http 400: {\"error\":{\"message\":\"model not found\"}}" table line 2: "p: delivered request cut by connection_lost after 21…" lines that look like the start of a row: 2 (one row was rendered) $ go test ./cmd/tmctl/ -run 'TestProbe_DeliveriesMarkNeverReachesTheReport' -
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО, причём я поднял доказательство с фикстуры на реальную запись в БД — у автора обе половины меряны на выдуманных строках, и одна из них была измерена НЕВЕРНО. Половина (а) — перевод строки доезжает до колонки. Сквозной прогон «обрыв → терминальный 403» (та же цепочка, что пиннит attemptcut_test.go:786) и чтение request_log из базы: calls=2 committed=0.001056 request_log[0]: cost=0.001056 estimated=1 finish=connection_lost newlines_in_err=1 len=160 err="fake http 403: {…}\nfake: delivered request cut by connection_lost after 24 body bytes after 4ms: unexpected EOF" renderReport печатает это фиксированными колонками, и одна запись выходит двумя строками, вторая — без колонок: out[1
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/tmp.66rCA9lVBc/backend/internal/pipeline/zzprobe_test.go "$d/backend/internal/pipeline/" && cp /tmp/tmp.66rCA9lVBc/backend/cmd/tmctl/zzprobe_test.go /tmp/tmp.66rCA9lVBc/backend/cmd/tmctl/zzprobe2_test.`
### P2 · `transport-and-config#5` — Пол zai замерен по одной модели из двух; вторая (флагман glm-5.1) наследует 35 ток/с без замера и без предупреждения
- **действие:** ЧИНЮ
- **где:** `configs/models.yaml:85-90`
- **как:** Пол zai замерен по одной модели из двух; glm-5.1 наследует 35 ток/с без замера. Либо замерить вторую, либо объявить отступление в комментарии, как сделано у deepseek.
- **тяжесть после опровержения:** minor · **источник:** линза+опровергатель
- **в чём дефект:** У deepseek правило соблюдено буквально: пол взят по САМОЙ МЕДЛЕННОЙ модели провайдера и рядом напечатан контроль по второй. У zai замерен glm-5 (647 строк), а glm-5.1 — строк ноль, при этом пол в конфиге провайдерский и достаётся ей целиком. Хуже, что молча: WARN «no measured generation speed for this provider» включается условием `c.profile.TokensPerSecFloor <= 0`, то есть объявленный пол его гасит — kimi без пола предупреждение получает, glm-5.1 с чужим полом не получает.
- **улика линзы:** Замер по 161 базе: `glm-5 rows=2194 usable=647 p10=37.51`; строк с model_actual/model_requested = 'glm-5.1' в request_log НЕТ (в таблице моделей всего четыре: deepseek-v4-flash 6361, deepseek-v4-pro 1745, glm-5 647, mistral-large-2512 14; kimi 0 — как и записано в комментарии kimi). Пробник: `m.AttemptDeadline("glm-5",16000) = 7m37.142857142s`, `m.AttemptDeadline("glm-5.1",16000) = 7m37.142857142s` — один и тот же вывод из замера, которого для второй модели не было.
- **почему опровергнуть не удалось:** ПОДТВЕРЖДЕНО, включая данные. Мой прибор прошёл по /home/ubuntu-26/projects/textmachine/books: `CONTROL: .db files found=162, opened=162, with a request_log table=161, open-failures=0`; агрегат по request_log: `deepseek-v4-flash rows=43894 usable(speed)=6361 max_prompt=4815`, `deepseek-v4-pro rows=11070 usable=1745 max_prompt=7563`, `glm-5 rows=2193 usable=647 max_prompt=7262`, `mistral-large-2512 rows=14 usable=14`. Строк glm-5.1 и kimi — НОЛЬ (их вообще нет в выдаче; контроль — четыре модели, которые есть). Пол провайдерский: мой пробник по боевому каталогу печатает `glm-5 tok_s_floor=35` и `glm-5.1 tok_s_floor=35`, DeadlineFor(16000)=7m37.142857142s у обеих. WARN действительно гасится: at
- **воспроизведение:** `cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/zzdbprobe_main.go /tmp/zzdbprobe/main.go 2>/dev/null || (mkdir -p "$d/backend/cmd/zzdbprobe" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/probes/zzdbprobe_main.go "$d/backend/cmd/zzdbprobe/main.go" && cd "$d/backend" && go ru`
## НЕ ЧИНЮ — пинг оркестратору и владельцу
Правило, по которому отобрано: правка трогает РАТИФИЦИРОВАННОЕ решение, продуктовую семантику или
размен, который выбирала не я. Чинить их своей рукой — то же самое, что я делала весь день и что
привело к 23 собственным дефектам.
### `transport-and-config#6` — Write-бонд рвёт ВСЮ h2-связь, а не застрявший стрим: соседний вызов, чьё тело уже ушло, умирает вместе с ним
- **где:** `internal/llm/httpllm.go:140`
- **почему не моё:** Write-бонд рвёт ВСЮ h2-связь (липкая cc.werr), а не застрявший стрим: соседний вызов, чьё тело уже ушло, умирает вместе с ним. Вместе с #2 и #9 это довод СНЯТЬ бонд, а не чинить — но снятие вернёт исходную дыру, и это решение оркестратора.
- **в чём дефект:** Ошибка записи липнет к соединению, а не к стриму, и h2 мультиплексирует параллельные стадии одного провайдера в один ClientConn. Один застрявший на записи вызов уносит все остальные in-flight на этом соединении. Деньги при этом не теряются (у соседа нет 2xx-объекта ⇒ Billable=false), но это лишний ретрай и лишняя задержка, и в комментарии к константе размен не назван.
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/prob`
### `transport-and-config#9` — Для тех тел, которые движок реально шлёт, write-бонд не может выстрелить — обещание «дедлайн был инертен» верно на два порядка выше нашего размера
- **где:** `internal/llm/httpllm.go:101-106`
- **почему не моё:** Для тел, которые движок реально шлёт, бонд выстрелить не может: обещание «дедлайн был инертен» верно на два порядка выше нашего размера. Кластер с #2 и #6.
- **в чём дефект:** Запись блокируется, только когда тело перестаёт помещаться в буферы сокета; до этого RoundTrip дописывает запрос, cleanupWriteRequest снова слушает ctx, и НАШ дедлайн работает штатно. Реальные тела запроса у нас — десятки KiB. Значит зафиксированный в комментарии класс зависаний достижим у нас только при теле в единицы MiB, чего пайплайн не отправляет; констатация полезна, чтобы следующая смена не считала бонд активной защитой боевых вызовов.
- **воспроизведение:** `d=$(mktemp -d) && (cd /home/ubuntu-26/projects/textmachine/backend && tar -cf "$d/t.tar" --exclude=./bin --exclude='./.env*' .) && mkdir "$d/backend" && tar -xf "$d/t.tar" -C "$d/backend" && cp /tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/prob`
### `критик#3` — Ранг флага решили, а ВЕРДИКТ главы — нет: одна остановка делает главе «attention», две — «fail»
- **где:** `internal/pipeline/status.go:716, :735, :737`
- **почему не моё:** Одна остановка делает главе вердикт «attention», две — «fail». Ранг флага это не лечит: вердикт считается по СЧЁТУ флагов, а не по их тяжести. Что должна показывать глава, над которой нажали стоп, — продуктовое решение.
- **в чём дефект:** Пак сам сформулировал риск в комментарии к рангу (status.go:443-448): «a passport that reported «cancelled» as a chapter's worst problem would hide the durable finding behind a transient one» — и закрыл его РАНЖИРОВАНИЕМ (FlagCancelled: 8, последний перед unknown). Но ранг влияет только на выбор WorstFlagReason (status.go:717-718). Счётчик UnitsFlagged инкрементится для ЛЮБОГО ChunkFlagged без взгляда на причину (:716), а вердикт главы считается по СЧЁТУ, а не по тяжести: `case p.UnitsFlagged >= 2: fail` / `== 1: attention`. Итог: оператор жмёт стоп в момент, когда в полёте две единицы одной главы, и паспорт объявляет главу проваленной — при том что следующий резюм обе строки перезапишет. На
### `критик#4` — Строка `cancelled` черновой стадии читается как ВЫПАВШИЙ ЧЛЕН редакторской единицы — контент-вердикт, вынесенный по факту нажатия стопа
- **где:** `internal/pipeline/status.go:538-543 → export.go:327`
- **почему не моё:** memberDrops опознаёт выпавшего члена по (черновая стадия && flagged) без взгляда на причину, поэтому строка `cancelled` читается как КОНТЕНТ-вердикт. Семантика экспорта — не моё решение.
- **в чём дефект:** memberDrops — по собственной шапке «the SINGLE definition of the c-lite rule «a dropped member flags its unit»» — опознаёт выпавшего члена ровно по паре (стадия черновая) && (disposition == flagged), без взгляда на причину. Новая строка cancelled ровно такова. Следствие: остановка над черновиком одного члена помечает ВСЮ редакторскую единицу как «член выброшен» (export.go:327 droppedAny → DispFlagged), то есть выносится вердикт о ТЕКСТЕ там, где с текстом ничего не случилось. Это тот же самый частично правленный носитель, что в находке №2, в третьем его месте, и запрет «флаг, лгущий о своей причине» пак цитирует сам (cutcall.go:158, D39.93 п.2). Ни линз, ни тестов, ни мутаций на memberDrops
## Порядок работы и правило остановки
1. **P0 целиком, одной волной** — это деньги. После каждой правки: `go test` затронутого пакета,
затем поштучная посадка мутации, которая её держит (краснота читается ПО ТЕКСТУ).
2. **P1** — числа, пины, гейты.
3. **P2** — операторские строки.
4. После всех трёх: полная `make battery` + `make mutations` + `counts.py --check`.
5. **Затем НОВЫЙ адверсариальный круг ПО ЭТИМ ПРАВКАМ.** Мерило смены: шесть кругов из семи находили
дефекты в починках предыдущего круга. Считать эту волну сделанной без такого круга — повторить
ровно ту ошибку, из-за которой этот наряд существует.
**Правило остановки для меня:** ни одна правка из PING-секции не делается без слова оркестратора или
владельца. Если по ходу P0P2 найдётся НОВОЕ — оно идёт строкой в этот же файл, а не чинится по пути.
## Где лежит сырьё — абсолютные пути (для оркестратора)
⚠ **Всё это живёт в /tmp и в каталоге сессии: умрёт вместе с сессией textmachine-79 или с рестартом
окружения.** Нужно пережить смену — переносить в репозиторий должен оркестратор, это его зона.
| что | путь |
|---|---|
| наряд (этот файл) | `/tmp/claude-1000/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/scratchpad/DOFIX-WORK-ORDER.md` |
| сырьё линз (JSON) | `…/scratchpad/lens-raw.json``{lenses: [6], critic: {…}}` |
| генератор наряда + карта диспозиций | `…/scratchpad/gen-order.py` |
| журнал воркфлоу | `/home/ubuntu-26/.claude/projects/-home-ubuntu-26-projects-textmachine/f88e2870-ec2e-48b8-9776-d0370e00bc0b/subagents/workflows/wf_c3ebfa7b-5d9/journal.jsonl` |
| ленты 13 агентов | тот же каталог, `agent-<id>.jsonl` (по 380790 КБ каждая) |
**Устройство прогона `wf_c3ebfa7b-5d9`: 6 линз → 6 опровергателей (по одному на линзу) → 1 критик
полноты.** Опровергатель читал находки СВОЕЙ линзы и валил их; критик получил все шесть отчётов
(промт 30 734 знака) и искал, чего не увидела ни одна линза. Своего опровергателя у критика не было —
поэтому его шесть находок в наряде помечены отдельно и не смешаны с 29 пережившими.
| лента | роль | предмет |
|---|---|---|
| `agent-a7d3181fa4bc9d229` | линза `money-predicate` | п.1 — разведение денежного предиката и предиката доставки |
| `agent-a4aa07eb0f4dcb581` | линза `burn-walk` | п.2 — перенос сжигания ключа внутрь `runAttempt` |
| `agent-ade92aafdb1c9d639` | линза `cancelled-mark` | п.3 — метка отменённой позиции в `defer` на все выходы `runStage` |
| `agent-ac3bcc26427aa8e1f` | линза `retry-loop` | п.4 — `withEarlierCut` и `cancelledDuring` |
| `agent-a1387fbd84e5a8b33` | линза `bank-and-counters` | п.5+7 — счётчик `Deliveries` и зонд `bankCheckpointExists` |
| `agent-a7bb6ad24c0e74f50` | линза `transport-and-config` | п.6+8 — `h2WriteByteTimeout`/`tuneHTTP2` и пол скорости `zai` |
| `agent-a9a5f3aa8f54cb667` | опровергатель | `money-predicate` |
| `agent-afb4ae3059cffa5eb` | опровергатель | `burn-walk` |
| `agent-a7b32987148b07e31` | опровергатель | `cancelled-mark` |
| `agent-a5c6d1b28a4c659f8` | опровергатель | `retry-loop` |
| `agent-a3556dae5726f1c1d` | опровергатель | `bank-and-counters` |
| `agent-a6375ed39378618cb` | опровергатель | `transport-and-config` |
| `agent-a26d3ffc70123b4a7` | критик полноты | все шесть отчётов разом |
Ключи блоков наряда (`линза#номер`) — это позиция находки ВНУТРИ отчёта своей линзы, так что от любого
блока можно дойти до сырья: `lens-raw.json``lenses[i].findings[n-1]`, оттуда — в ленту линзы.