textmachine/docs/BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md

30 KiB
Raw Blame History

Бэкенд-пак: КУПЛЕНО — ЗНАЧИТ ДОСТАВЛЕНО. Юнит, который не доделается НИКОГДА

Выдан оркестратором №23 (сессия textmachine-11) 11.09. Пак $0 — платных вызовов в нём нет. Предмет решён владельцем 05.09 (D39.204); ты строишь решённое, а не выбираешь между вариантами.

1. Какая проблема и что решит твой результат

Читатель заплатил за главу. Движок купил первую попытку, получил флагуемый результат, пошёл за второй — и вторую ему не продали: потолок книги или дня не вмещает удвоенный бюджет регенерации (maxTokensForAttempt удваивает на каждой, значит попытка 1 резервирует вдвое больше попытки 0).

Дальше происходит вот что, и это проверяется одной строкой: backend/internal/pipeline/stagerun.go:204=infra failure; the cancelled-position mark — ЛЮБАЯ ошибка попытки уходит наверх из цикла, РАНЬШЕ UpsertChunkStatus.

Следствия два, и оба стоят денег читателя:

  1. Юнит не доделается НИКОГДА. Чекпойнт попытки 0 есть (реплей за $0), строки chunk_status НЕТ ⇒ читающие модели видят юнит как pending, экспорт не отдаёт ничего. Каждый резюм реплеит попытку 0 за $0 и умирает на попытке 1. Прогон уходит exit 4 при ПОЛНОСТЬЮ доставленных купленных юнитах.
  2. Волну нельзя продолжить без ДЫРЫ в непрерывном префиксе — то есть за это платят и соседние юниты, которые в бюджет влезали (разброс резервов внутри волны замерен ×2.976).

Решение владельца (D39.204, и оно НЕ обсуждается): отказ резервации на РЕТРАЕ деградирует во ФЛАГ, причина которого ОТЛИЧНА от исчерпания регенераций; текст попытки 0 оплачен и отгружается; волна идёт дальше. Вариант «минимальная покупка ≥ step_max_usd» СНЯТ владельцем — он стоил бы покупателю входного порога в пять глав. Не реализуй его и не предлагай.

И канон здесь сильнее, чем кажется: D2 п.2 уже ТРЕБУЕТ тегировать flag_reason. Значит новая причина — не добавка к контракту, а его ИСПОЛНЕНИЕ. (Ссылка «смена D2.3», гулявшая по бэклогу, неверна дважды: D2 и так кончается флагом, а D2.3 — про детектор вырождения перед удвоением.)

2. Зона записи и git

  • Зона — backend/, и только она. platform/ не трогать: её половина едет ОТДЕЛЬНЫМ промтом, но ОДНИМ АКТОМ с твоим (см. §4.3).
  • Ты не коммитишь. Готовое дерево передаёшь мне, я лендлю. git add -A запрещён каноном.
  • Свою секцию итогов пиши в docs/PROGRESS.md — это единственное исключение из «docs/ — зона оркестратора». Чужие незакоммиченные файлы в дереве не трогай.

3. Карта чтения — ЗАКОН, дальше только по её ссылкам

  1. CLAUDE.md — целиком.
  2. Тело D39.204 в docs/architecture/05-decisions-log.md (решение владельца) и D2 п.2 — он в закрытой эре, тело в docs/archive/architecture/05-decisions-D1-D38.md, греп ^## D2\.. ⚠ Подномер собственного тела не имеет: D2.3 — это ПУНКТ 3 тела D2.
  3. Ряд 291 в docs/BACKLOG.md — там весь разбор, включая опровергнутую посылку прошлого оркестратора: «ретрай оставляет ФЛАГОВАННЫЙ юнит» — ЛОЖЬ, зона опровергла по строкам. Не наступи второй раз.
  4. backend/internal/pipeline/stagerun.go (цикл попыток и runAttempt), escalation.go (errReserveCeiling и кто на нём УЖЕ деградирует), disposition.go (словарь FlagReason).
  5. docs/architecture/12-go-style-notes.md — нормативы стиля, только больные места.

Код-факты добывай сам; file:line выше — отправные точки. Код первичен.

4. Состав

4.1 Деградация — делай РОВНО так в ЧАСТИ РЕШЕНИЯ, сам решай КАК

Ровно так (решено владельцем, отступление = пинг):

  • отказ резервации на попытке ≥1 при существующем оплаченном результате попытки 0 ⇒ флаг, а не ошибка наверх;
  • у флага СВОЯ причина, отличная от исчерпания регенераций: читатель и оператор обязаны отличать «мы перепробовали всё» от «денег не хватило на вторую попытку»;
  • волна идёт дальше.

ЧЕГО В ЭТОМ СПИСКЕ БОЛЬШЕ НЕТ И ПОЧЕМУ — читай, это главное место пака. Первая редакция промта требовала, вслед за D39.204 п.4, чтобы «текст попытки 0 отгружался». Это невыполнимо как написано, и поймал это опровергатель, а не я. В цикл ретрая попадают ТОЛЬКО флагованные попытки ретраебельных причин — length/empty плюс эхо-реролл (backend/internal/pipeline/disposition.go, греп retryable). Значит «оплаченный текст попытки 0» по построению обрезан, пуст либо является эхом исходника. А флагованная диспозиция (кроме FlagSanitizerStripped) оставляет пустой финальный текст, следующая стадия скипается, и экспорт не отдаёт ничего — backend/internal/pipeline/holes.go называет это withheld прямо: «contaminated output never ships» (D2).

настоящий выигрыш пака формулируется так, и он не меньше прежнего, а честнее: невидимая дыра pending (строки нет · юнит заклинен · exit 4) → НАЗВАННАЯ дыра withheld (строка есть · причина денежная и названа · волна идёт · прогон кончается штатно). ⚠ Отгрузка обрезанного текста — ОТДЕЛЬНОЕ продуктовое решение, и я задал его владельцу. Придёт «да» — доедет релеем отдельным сообщением, и ты подтвердишь его эхом. Сам не строй.

И ВТОРОЕ, без чего пак сделает читателю хуже: ДЕНЬГИ ОБЯЗАНЫ ОСТАТЬСЯ ВИДНЫ. Сегодня отказ ретрая публикует денежный факт: событие ceiling + защёлка + exit 4, и платформа пишет прогону paused и просит пополнить. После деградации этот путь исчезает: терминальные ветки flagged и clean несут только объём, а Money висит на ошибочных (backend/internal/pipeline/events.go, греп terminal). Читателю кончились деньги — и никто ему этого не говорит.делай РОВНО так: прогон, отказавший только РЕТРАЮ, по-прежнему публикует денежный факт — либо явно НЕ публикует, и тогда довод записан в отчёте и в коде. ⚠ И рядом живёт комментарий, который твой пак сделает ложным, если факт пропадёт: runevents.go, греп ITS PRESENCE RULE — «правило присутствия — „потолок был достигнут“, а не „исход = ceiling“».

Решаешь сам и аргументируешь: имя причины · где ставится деградация. И вот ШТАТНЫЙ носитель, который первая редакция промта пропустила: у runAttempt уже есть параметр mandatory — «позволено ли отказанной резервации ЖДАТЬ освободившихся денег»; волна зовёт его с true, а эскалационный хоп, под-шаг ремонта и терминология зовут с false и потому деградируют (backend/internal/pipeline/stagerun.go, греп mandatory; правило — reservegate.go, греп waitNotAllowed). Это ровно тот класс, под который механизм построен. ⚠ Но у него есть цена, и её надо назвать прежде, чем брать: mandatory=false отнимает не только остановку, но и ПРАВО ЖДАТЬ — ретрай деградирует даже там, где деньги освободились бы через миллисекунду сеттлом соседа. Остановка и ожидание — два решения на одном флаге.

Попытку 0 не трогать: её отказ по-прежнему ОСТАНАВЛИВАЕТ книгу — ратифицировано, менять нельзя.

Гипотеза, которую надо ПРОВЕРИТЬ, а не принять: деградация сама по себе закрывает и следствие (2) — у юнита появляется статус, значит дыры в непрерывном префиксе нет. Похоже на правду; предъяви замером.

4.2 Пин, ради которого пак существует

«Купленное доставлено ⇒ не exit 4». Прогон, где каждый купленный юнит доставлен, а один из них доставлен ФЛАГОВАННЫМ из-за нехватки денег на ретрай, обязан кончиться exit 2 (флаги есть, но прогон завершён), а не exit 4. ⚠ Не «успехом»: exit 0 требует, чтобы флагов не было вовсе, а флаг здесь есть по построению — это и есть честный исход.

И назови СУЖЕНИЕ, иначе будешь ловить недостижимое: утверждение верно в ОКНЕ, когда остатка потолка хватает на попытку 0 каждого следующего юнита. Кончился потолок совсем — попытка 0 следующего юнита обязательна, отказ по ней штатно останавливает книгу, и exit 4 возвращается ЗАКОННО. Фикстуру строй именно на этой развилке.

ПИН НА РЕЗЮМ — И ЗДЕСЬ ПЕРВАЯ РЕДАКЦИЯ ПРОМТА ЗАПИНИЛА БЫ ОТМЕНУ РАТИФИЦИРОВАННОГО, не заметив. Я требовал «после такого прогона резюм не делает НИ ОДНОГО свежего платного вызова». Но денежный флаг — единственная причина, которую ЛЕЧАТ ДЕНЬГАМИ: сегодня «поднял потолок → резюм → читатель получил хороший перевод» ратифицировано (backend/internal/pipeline/escalation.go, греп durably pauses and resumes, D4). Мой пин сделал бы юнит навсегда деградированным, лечимым только редрайвом. ⇒ делай РОВНО так: денежная причина ПЕРЕ-АТАКУЕМА на резюме — как FlagCancelled, единственная причина, у которой resolvedForResume ложен именно потому, что это «оплачено, но не сделано» (backend/internal/pipeline/cutcall.go, греп resolvedForResume). Пинь ДВЕ вещи: без поднятия потолка резюм не делает свежих платных вызовов и не умирает · после поднятия потолка резюм ДОДЕЛЫВАЕТ юнит.

Два гейта поймают тебя сами — назову, чтобы не терять круг: TestEveryFlagReasonIsRanked потребует завести новую причину в flagSeverity (backend/internal/pipeline/status.go), а TestEveryOperatorMessageIsCatalogued — внести новое сообщение в backend/internal/pipeline/testdata/operator-messages.txt.

4.3 Платформенная половина — твоё дело назвать, не построить

Карта заметок платформы РУКОПИСНАЯ (platform/internal/ingest/notes.go) и уезжает независимо от словаря движка. Цена несделанного замерена: PD-246 — минор 0.10.0 сутки жил ЛОЖНЫМ на проводе, причина доезжала читателю как unspecified, и поймала это зона своей пере-проверкой ПОСЛЕ слов оркестратора «приёмка закончена».

отдельным пунктом отчёта назови мне: точное значение новой причины на проводе · что платформа обязана записать прогону, который кончился флагом вместо exit 4 · какой её пин это стережёт. Я передам это платформенной сессии, и акт будет один на две половины.

4.4 Приложение — ДВА пина, краснеющих по УДАЧЕ (ряд 379, ОБЕ половины)

Отдельная от §4.1 работа; делай её ПОСЛЕ основной. И мой диагноз здесь был НЕВЕРЕН — опровергнут замером, а не мнением. Читай, чем именно, иначе повторишь мою ошибку.

Половина (б). Запись каталога CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write стережёт самую дорогую половину денежного бита delivered() (backend/internal/llm/attemptcut.go:189=answered && t.firstByte.Load()): провайдер ОТКАЗЫВАЕТ и рвёт коннект, пока наше тело ещё пишется — байт ответа есть, статуса в руках нет, и чтение «байт = доставка» оплачивает сметой каждый отказ (замер прежнего пака: 22 отказа из 25, один на $0.80).

Два независимых замера на посадке (снятие answered &&), и они расходятся: мой — 7 красных из 8 (снят на нагруженной машине, рядом шла батарея); опровергателя на спокойной — 8 из 8, а на сорока процессах 39 FAIL / 1 PASS. ⇒ пин краснеет по УДАЧЕ, это подтверждено обоими; частота зависит от нагрузки, и поэтому «восемь красных подряд» не является критерием приёмки: сегодня, без единой правки, восьмёрка выпадает примерно в 82 случаях из 100.

А вот ПРИЧИНА не та, что я написал. Я заказывал «сделать приход байта ответа НЕИЗБЕЖНЫМ до обрыва». Зонд опровергателя (40 итераций, печать того, что реально получил клиент) дал 39 × обрыв с отказом записи и 1 × ПОЛНЫЙ 401, и ни разу — «ни куска, ни статуса». То есть байт приходит практически всегда, а мутант выживает в ДРУГОМ случае: клиент успевает вычитать 401 ЦЕЛИКОМ, Do возвращает не ошибку записи, а ОТВЕТ, и delivered() не спрашивают вовсе — ошибка уходит статусной веткой. Моё лекарство било по половине, которая и так держится, и увеличивало вероятность ровно того исхода, в котором мутант выживает. (И «~6 МБ тела» тоже неверно: 11.0 МБ — Go не эскейпит кириллицу в JSON.)

Задача — делай РОВНО так в части ЧТО, способ твой:

  1. Тест утверждает СВОЮ ПОСЫЛКУ и печатает её: что Do вернул ошибку ЗАПИСИ и что трасса видела первый байт. Сценарий не состоялся ⇒ тест КРАСНЫЙ, а не зелёный «мимо предмета». Сегодня он зелен и на полном 401, то есть на другом предмете.
  2. Прямой табличный пин на сам delivered() — он убивает посадку answered && детерминированно, без гонки вообще.
  3. У фикстуры MaxAttempts: 3 — до трёх бросков кости на один Complete; границу утверждай ПО ПОПЫТКЕ, а не по итогу вызова.

Половина (а) — её первая редакция промта не заказывала вовсе. Тот же ряд 379 несёт вторую половину: две строки таблицы девяти в backend/internal/pipeline/cutcall_test.go:402=time.Sleep(50 * time.Millisecond) разводятся ОКНОМ СТЕННЫХ ЧАСОВ, и по какую сторону границы заголовков сел cancel(), не утверждается ничем — замер приёмки дал 2 красных из 8. Ряд закрывается ТОЛЬКО вместе с этой половиной.

Критерий приёмки берётся из самого ряда 379, он сильнее моего: «каждый из двух пинов ПЕЧАТАЕТ величину, доказывающую пройденную границу, и ПАДАЕТ, когда сценарий не состоялся».

5. Где этот пак мягкий — четыре места, назвал я, веер и глубину выбираешь ты

Мандат самопроверки — исполнением; субагенты разрешены явно. Адверсариальный проход по СВОЕЙ ГОТОВОЙ работе обязателен.

  1. Пин, утверждающий только молчание, вакуумен ровно там, где сообщение ложно. «Ошибка не вышла наверх» — утверждение о молчании. Фикстура обязана быть такой, где новая причина ОБЯЗАНА прозвучать, и тест утверждает её ИМЯ.
  2. Фикстура обязана сделать отказ резервации НЕИЗБЕЖНЫМ на попытке ≥1 и НЕВОЗМОЖНЫМ на попытке 0. Денежный пин, флейковый на мутанте, измеряет пустой сценарий: у нас уже был пин, дававший 2 красных из 8, потому что деньги были ВЕРОЯТНЫ, а не неизбежны. Гоняй свой не один раз.
  3. Посадка обязана бить в тот же слой, что стережёт пин. За прошлый пак мутация трижды оказалась не про то: недостижимое условие в фикстуре · не собирающаяся посадка (НЕизмеренная — хуже выжившей) · код против гейта над данными.
  4. Новая причина — факт ПРОВОДА, а не словаря. Тест, проверяющий только, что константа добавлена в FlagReason, не доказывает, что читатель её увидит. Скажи, чем предъявлена дорога от флага до отчёта/экспорта.

⚠ И общее, стоившее трёх смен подряд: зелёная батарея плюс полный мутационный каталог сходимостью НЕ являются. В последних трёх паках направленный второй читатель находил 6, 7 и 9 дефектов при полной зелени.

Адверсариальная стойка — способ смотреть, не правило пака

  • Автор и ревьюер — разные роли, даже когда это один ты. Перечтение своей работы рубежом не считается: поднимай читателя, которому НАЗВАНО, где мягко.
  • Спрашивай у аномалии, о ЧЁМ она — о предмете или о твоём приборе.
  • Верный результат при неверном методе не краснеет нигде. «Сошлось» — не доказательство.
  • Утверждение о молчании вакуумно без фикстуры, где оно ОБЯЗАНО прозвучать.
  • Заимствованное число проверяется не на существование, а на ТУ ЛИ КЛЕТКУ: та же роль, та же модель, та же стадия, тот же режим?
  • Главный вопрос отчёта — не «что не получилось», а «что ты знаешь и не сказала».

Находки ВНЕ заказа — отдельный заказ владельца

Слово владельца 11.09: находить проблемы даже там, где мы не ждём. Задавай себе в каждой фазе вопрос, у которого нет заранее известного ответа: что здесь стоит дороже, работает хуже или ведёт себя страннее, чем должно бы, — и о чём никто не спрашивал? Ради чего он заведён: удорожание из-за падающих запросов на НАШИХ настройках никто не искал — просто кто-то посмотрел в леджер без гипотезы и увидел 25.4 % цены книги в выброшенных попытках, при зелёных тестах и молчащих гейтах.

Докладывай отдельной секцией: криты · мажоры · регрессии · баги · жёсткие точки улучшения — каждое аргументированно, с носителем (file:line, число, команда) и с ценой в деньгах, тексте или доверии читателя. ⚠ Секция обязана быть непустой ИЛИ нести строку «искал вот так, не нашёл» с перечислением мест: «находок нет» и «не смотрел» в отчёте выглядят одинаково.

6. Предметные оси ревью

Выбери 13 и назови какие. Мой приор: деньги под гонкой · терминальное состояние прогона (что видит платформа и что видит человек) · обратная совместимость словаря причин.

7. Записка-план ДО работы

Перед первой правкой — записка: что меняешь, чем предъявишь, где ждёшь сопротивления. Комплектность против заказа сверяй механически.

8. Заявление = команда

Каждое число и каждая категорика отчёта — с командой, которой получены. Дифф ^func Testисполнением, не памятью. Приёмка пере-снимает.

9. Эхо-протокол старта

Первым действием — ≤10 строк СВОИМИ словами: что понял · что считаешь опасным · что считаешь неверным. Не пересказ: дословный пересказ подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним.

Адрес бери из /tmp/textmachine-channel — блок с role=оркестратор, — а не из этого промта: имя сессии не переживает рестарт окружения. Перед отправкой сверься с ListAgents: файл переживает смерть сессии, а ListAgents — нет.

10. Что НЕ удалось — обязательная секция отчёта

И вторая её половина: «где прибор слеп и я это знаю» — что невидимо · почему не чинил · чем закрывается. Норма прошлой смены: «в коде названо, в отчёте нет — значит для следующей смены НЕ названо».

11. Канал вопросов и право отказаться

Конфликт промта с кодом или доками — пинг мне, не интерпретация. Право сказать «этого делать не надо» с аргументом у тебя есть и им пользовались: в двух случаях из трёх правы были сессии, а не я.

Тесты и гейты под зелень не подгонять. Правка, вызванная ЗАКАЗАННОЙ сменой поведения, — обслуживание, и протухший тест держать не нужно; но она ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось, какой тест это описывал, куда уехала гарантия.

12. Прямой канал

Механизм — CLAUDE.md §«Связь между сессиями». Впиши свой блок в /tmp/textmachine-channel ПЕРВЫМ действием. Нужной роли нет ⇒ канала нет, и это нормальный случай: НЕ опрашивай сессии подряд.

13. Критерий завершённости

У каждого пункта заказа — исход (сделано · не делаю с доводом · пинг) · круги СОШЛИСЬ (последний не дал НОВЫХ находок; прежние закрыты таблицей «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО») · таблица мутаций полная, выжившие названы, и КАЖДАЯ новая запись помечена battery (прошлый пак этого не сделал, и его «21/21 RED» оказался разовым прогоном смены, а не гейтом проекта) · числа сняты ПОСЛЕ последней правки · всё живое в ДЕРЕВЕ, а не в письме · явное «работа завершена, править не планирую». Без последнего пак считается идущим.