From 1c5bd2a00fa6bafee9060ef6054608f29ceb9857 Mon Sep 17 00:00:00 2001 From: heaven Date: Thu, 10 Sep 2026 23:51:18 +0300 Subject: [PATCH] Bring the announced contract version to the canon the corrective minor moved, and keep the platform zone's account of what it learned before the rollback. --- platform/docs/platform-PROGRESS.md | 166 ++++++++++++++++++++++ platform/internal/httpapi/capabilities.go | 7 +- 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index d7a82129..49aa6c89 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -49,6 +49,172 @@ committed_usd, which is what would let this side say "at most Y"». Движок чтение оценочных строк из потока. Промт ещё не написан — если у вас есть довод против любой из четырёх позиций, скажите ДО того, как я его напишу. +## ПАК «ДВЕ ОСТАНОВКИ» — ОТМЕНЁН ВЛАДЕЛЬЦЕМ, РАБОТА ОТКАЧЕНА (10.09, `textmachine-5c`) + +> Промт `docs/PLATFORM_SOFT_STOP_SESSION_PROMPT.md` (редакция `ac9a24d`), вход HEAD `ac9a24d`. +> **Отменён владельцем по размаху**, а не по качеству: две остановки того не стоят. Новый инвариант +> объявлен тем же словом: **стоп остаётся ЖЁСТКИМ, терять деньги ДОПУСТИМО, но остановка обязана быть +> КОРРЕКТНОЙ** — без гонок и половинчатых состояний, с правильным возобновлением. Предмет переехал с +> «не сжечь деньги» на «не оставить мусор». +> +> ⚠ **Эта секция — не отчёт о построенном, а НАЙДЕННОЕ.** Код зоны возвращён к HEAD целиком (кроме +> константы версии контракта, см. ниже). Здесь остаётся то, что зона УЗНАЛА о собственном дереве: +> оно пережило пак и стоит дороже, чем стоил бы код. + +### Что откачено и что оставлено + +Возвращены к HEAD **26** путей в `platform/`. Удалены девять моих новых файлов: +`internal/gates/stopgrace_test.go` · `internal/httpapi/stopmode_test.go` · +`internal/pgstore/migrations/00035_stop_mode.sql` · `internal/pgstore/migrations/00036_estimated_and_stop_ledger.sql` · +`internal/pgstore/stopmode_test.go` · `internal/runner/stop_systemd_test.go` · `internal/runner/stop_test.go` · +`internal/runs/estimated_test.go` · `internal/runs/stopmode_test.go`. + +**Оставлено ОДНО и по прямому указанию оркестратора:** `internal/httpapi/capabilities.go`, константа +`ContractVersion`. Это чужой долг, взятый попутно: канон уехал на `0.13.1` актом `D39.235` п.6, а +константа осталась на `0.13.0`, и гейт `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` +краснел с момента ратификации — базовая линия зоны была КРАСНОЙ на входе, и не по вине пака. +⚠ В ходе пака константа стояла на `0.14.0` (свой минор: `mode` у `/stop` плюс `Run.stop_mode`, по +ратифицированному порядку «код первым»); с отменой пака этого минора нет, поэтому при откате она +приведена к **`0.13.1`** — к тому, чему билд действительно служит. + +### ⛔ Что зона узнала о СЕБЕ — это и есть результат смены + +Ниже разделено на «замерено исполнением», «прочитано в коде» и «латентно» намеренно: смешивать их +дороже обычного, потому что по ним заводятся строки. Адреса — по HEAD `ac9a24d`. + +**1. `Runner.Stop` блокируется на весь грейс — ЗАМЕРЕНО.** `internal/runner/runner.go:195` зовёт +`systemctl --user stop` без `--no-block`, `runCommand` (`:106-112`) своего тайм-аута не имеет. +Пробник на транзиентном юните с `TimeoutStopSec=15`, процесс ловит SIGTERM и не выходит (systemd 259): +блокирующая форма вернулась через **15,058 с** (по SIGKILL), `stop --no-block` — через **0,014 с**, +юнит в `deactivating`. Сегодня движок на жёстком сигнале умирает за секунды, поэтому это невидимо. +Носители последствия: `WriteTimeout: 30 s` (`cmd/tmplatformd/main.go:208`) и свип (п.2). + +**2. Израсходованный бюджет прогона свип считает ПРОВАЛОМ реконсиляции — прочитано в коде.** +`reconcileOne` (`internal/runs/reconcile.go:89-90`) заворачивает каждый прогон в `s.runBudget()` +(дефолт 60 с), и комментарий на `:92-115` говорит буквально «SPENDING the budget counts as a failure +even when nothing reported one». ⇒ блокирующий `Stop` внутри свипа (`:580`) даёт `ReconcileFailures++`, +отсрочку и через `StalledAfter` ЛОЖНУЮ тревогу оператору, плюс съеденную долю прохода для всех +остальных аккаунтов. ⚠ Прежняя формулировка оркестратора («свип встанет до двадцати минут») неверна +по механизму и исправлена актом `D39.236` п.5 по возражению этой смены. + +**3. Повторный `stop` на юните в `deactivating` ИНЕРТЕН — ЗАМЕРЕНО, и это несущее свойство.** +Три стопа в t+0 / t+2 / t+4 против грейса 6 с: процесс получил **один** сигнал, юнит умер на +**t+6,04 с** (не t+10). То есть пере-выдача не шлёт второго сигнала и не перезапускает +`TimeoutStopSec`. ⭐ Ценность не в хорошей половине, а в том, что комментарий свипа +(`reconcile.go:576-579`) УЖЕ утверждает «asking again is free and idempotent», и это утверждение +держится на недокументированном поведении systemd, которое до этой смены никто не проверял. +⚠ И вторая половина того же комментария — «it is the only thing that closes the first case» — +**не выполняется**, если юнит уже начал останавливаться: пропущенный первый SIGTERM свип не чинит, +юнит стоит в `deactivating` до SIGKILL по грейсу (сегодня — до десяти минут прогона, который ничего +не делает, с зарезервированным холдом). + +**4. Два SIGTERM подряд Go-процесс видит как ОДИН — ЗАМЕРЕНО, ратифицировано `D39.238`.** +gap 0 мс → оба сигнала увидены в **0 из 20** прогонов (в длинной серии слиплись 40 из 40); +gap 1 мс → **20 из 20**; буфер принимающего канала **8** — коалесценция живёт НИЖЕ канала, в +рантаймовом БИТЕ на сигнал. ⚠ Первый мой прибор давал «2 из 2» и подтверждал удобное: `kill` шёл из +шелла, и запуск двух процессов сам создавал зазор. Прибор чинен, число перевернулось. +Норма, выведенная из этого оркестратором: **замер, чей результат подтверждает удобное, проверяется на +ПРИБОР прежде, чем на предмет.** + +**5. ⛔ `finish` рассчитывает деньги из ДО-ДРЕНАЖНОЙ выписки — ЛАТЕНТНО, но класс настоящий.** +`reconcile.go:955-958`: расчёт внутри `finish` объявлен оппортунистическим и получает тот самый `l`, +который проход прочитал ДО дренажа журнала. `finish` вручную освежает `l.Status`/`l.PausedReason` +перед вызовом (`:~918`) — то есть автор знал про класс и закрыл ровно два поля. **Любое число, которое +дренаж ЭТОГО ЖЕ прохода записал в строку попытки, для этого расчёта невидимо.** +⚠ Граница честности: на HEAD ничего не сломано — `settle` не читает из выписки ни одного поля, которое +пишет дренаж. Класс предъявлен исполнением только потому, что пак добавил такое поле: строка попытки +читала 9 оценочных строк, выписка — 0, и расчёт взял число, которое случайно знал запасной канал. +Поле ушло с паком, класс остался. Родственник в этом же файле уже стоил зоне дефекта: `freshRunState` +(`reconcile.go:~660`) написан ровно потому, что выписка не может знать, что записал её собственный дренаж. + +**6. Обработчик `/stop` шлёт сигнал по имени юнита из СВОЕЙ ЖЕ выписки — прочитано в коде.** +`Service.Stop` (`reconcile.go:1541`) берёт `unit` из `RequestStop` и зовёт `Runner.Stop(ctx, unit)`. +Успел ли свип между этими двумя рестартовать попытку — сигнал уйдёт СТАРОМУ юниту. Самолечение есть +(следующий проход увидит `alive && StopRequestedAt != nil` и пере-выдаст стоп текущему, `:575-583`), +но приходит оно от СВИПА, а не от обработчика: окно равно одному тику, и всё это время пользователь +держит `202`, а прогон работает. + +**7. Двойной клик по `/stop` — гонка по построению, сегодня безвредная.** `RequestStop` +(`internal/pgstore/runs.go:1298-1309`) — один UPDATE; под READ COMMITTED два одновременных нажатия +не различимы самим оператором, потому что `RETURNING` отвечает НОВОЙ строкой и не может отличить +собственную запись от чужой. Схлопывание в первый таймстамп делает оба нажатия одинаковыми, поэтому +вреда нет — ровно до той секунды, когда ответ или действие начнут зависеть от того, кто нажал первым. +Форма лечения, проверенная в отменённом паке: `select … for update` в одной транзакции с UPDATE. + +**8. `stoppedOnRequest` сравнивает ДВОЕ РАЗНЫХ ЧАСОВ, и код это признаёт.** `reconcile.go:1137-1153`, +довод на `:~1000`: «the two clocks being compared are not even the same one — the marker carries the +host's, the intent the platform's». Сегодня цена ошибки — ЯРЛЫК. В любой конструкции, где от этого +ярлыка зависит ДЕЙСТВИЕ (перезапуск, возврат холда, возобновление), это гонка по построению. + +**9. Каталог движка: потолка ожидания нет у пяти провайдеров из восьми — ЗАМЕРЕНО прибором.** +Программа на `config.LoadModels` + `Timeouts.Profile().DeadlineFor(B)` над снапшотом `git archive HEAD +backend` (ни байта записи в чужую зону): `attempt_max_s` задан у **3** (`deepseek` 1240 · `kimi` 1200 · +`gemini` 1200), инертен у **5** (`zai` · `xai` · `openai` · `mistral` · `local`), и у них `DeadlineFor` +растёт с бюджетом линейно и сверху не ограничена. Максимум по каталогу при наибольшем бюджете +сегодняшней конфигурации (**35 200** ток. = редакторские `max(8000×2.2, 16000) = 17 600` с одним +удвоением по `regenerate_before_escalate: 1`) — **1240 с**, deepseek. Беcключевой `zai` перерастает его +при **43 400** ток., то есть при `edit_ceiling_out` ≈ 9 900 против сегодняшних 8 000 — одна строка +yaml, которая читается как ручка качества. Носитель — строка бэклога **383**. + +**10. Dev и прод расходятся по грейсу в 20 раз, и это не объявлено нигде.** +`internal/ingest/supervisor.go:18` — `stopGrace = 30 * time.Second` (уезжает в `cmd.WaitDelay`, `:75`) +против `600 с` в проде (`internal/runner/runner.go:59`). Один и тот же движок против тех же платных +провайдеров получает на свёртку полминуты на dev-пути и десять минут на боевом. + +**11. Пять открытых рядов реестра — про холды и остановку, и это один предмет, а не пять.** +Их печатает сам гейт зоны (`make check`, `internal/gates`, строка `ALARM PD-count: 15 open rows … +(baseline 15)`): **PD-418** (у settling-строки живого прогона нет ручки, а рантбук обещает обратное) · +**PD-244** (единственный выход `settle`, оставляющий холд открытым МОЛЧА) · **PD-162** (книга с +пропавшим каталогом заклинивает прогон навсегда с открытым холдом) · **PD-465** (заявка на спавн +коммитится ДО подъёма юнита) · **PD-217** (незакрытый холд блокирует апгрейд движка бессрочно). +Под новым инвариантом это готовый предметный список, уже приоритизированный реестром. + +**12. Что в дереве оказалось ЛУЧШЕ ожидаемого — и это тоже находка.** Гонка «кто пишет исход мёртвого +прогона» ЗАКРЫТА, и закрыта правильной формой: `FinishRun` возвращает `closed bool`, и `finish` +(`reconcile.go:903`, ветка `!closed` ~`:937`) разбирает случай «прогон уехал под этим проходом» явно; +второй расчёт получает `ErrNoReservation`, потому что резервация закрывается под `state = 'open'`. +Столкновение реконсайлера с `/stop` названо в трёх местах и в каждом закрыто: `finishStopped` +(`:678-685`), отказ рестарта по `ErrStopRequested` (`:1324`), пере-проверка заявки под замком в +`FinishUnspawnedStop` (`:~710`). ⇒ **модель «записать условно и прочитать вердикт» уже живёт в зоне**, +и новому инварианту стоит брать её образцом, а не изобретать другую. + +**13. И одна развилка, которую новый инвариант обязан пересмотреть ЯВНО.** +`interruptedBySomeoneElse` (`reconcile.go:539`, функция `:1129`) ПЕРЕЗАПУСКАЕТ прогон по коду 5 без +записанного намерения. Это осознанное решение с записанным доводом (ребут против ручного +`systemctl stop`, асимметрия цен), а не дефект — но это главная развилка «правильного возобновления», +и наследовать её молча нельзя. + +### Числа смены + +``` +$ make check (все четыре гейта стенда закрыты: Postgres 55433 · движковый бинарь и шаблон из + `git archive HEAD backend` · достижимый пользовательский systemd · MemoryMax) + вход (до первой правки): MAKE-EXIT=2 · 19 `ok` · 1 FAIL · линтер «0 issues» · скипов 5 + единственный красный — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` + («this build announces contract 0.13.0 and the ratified canon is 0.13.1»), НЕ по вине смены + скипы 5, условие у всех одно и названное: нет деплой-артефакта `configs/mining-contrast.zh.txt` + + ПОСЛЕ ОТКАТА (последний прогон, снят после последней правки): + MAKE-EXIT=0 · 20 `ok` · 0 FAIL · линтер «0 issues» · скипов 5 + `gofmt`/`go vet`/`sqlc diff` чисты · `ALARM PD-count: 15 open rows … (baseline 15)` — не сдвинут + красный входа ПОГАШЕН правкой константы: канон `0.13.1`, билд `0.13.1` +``` +⚠ Гейты стенда закрыты все четыре намеренно: без `TM_PLATFORM_TEST_DSN` та же батарея печатает те же +`ok` и прячет ~370 тестов (эррата `08.09-д`). Счёт скипов печатается рядом со счётом `ok`. + +### Что НЕ удалось + +**Ничего из заказанного не сдано — пак отменён на середине, и это не оценка работы, а факт.** +Построенное и зелёное на момент отмены (грейс суммой с названными слагаемыми, `--no-block`, путь +второго сигнала через наблюдение `SubState`, режим в записи намерения с атомарной заявкой на второй +сигнал, чтение минора `1.4` из обоих каналов, пометка оценки на строке расчёта, живой systemd-стенд) +откачено целиком. Из адверсариального прохода §5.5 успели отработать три направления из шести — +(а) гонка двух нажатий, (б) остановка мёртвого юнита, (д) пере-выдача не должна эскалировать; (в) +чтение мягко остановленного прогона реконсиляцией, (г) идемпотентность `/stop` и (е) удлинённое окно +`deactivating` до отмены проверены не были. Мутационная проверка СВОИХ новых пинов — та, которую +норма зоны требует отдельным заходом, — не выполнена ни для одного из них: до неё смена не дошла. +Поэтому ни один пин выше в дереве не остаётся, и ни на один нельзя ссылаться как на проверенный. + ## Состояние зоны на 08.09.2026 | Вопрос | Ответ | diff --git a/platform/internal/httpapi/capabilities.go b/platform/internal/httpapi/capabilities.go index a633d2c2..567f0ef2 100644 --- a/platform/internal/httpapi/capabilities.go +++ b/platform/internal/httpapi/capabilities.go @@ -36,7 +36,12 @@ import "net/http" // the bar described in chapters while the wire sent units, `ordered_chapters: 0` promised where the // wire sent 2, three values of a vocabulary that had four — because the NUMBER matched each time. The // bump keeps the number from lying; it does not close the blind spot, which is unified backlog row 309. -const ContractVersion = "0.13.0" +// +// ⚠ 0.13.1 is the canon's CORRECTING minor (D39.235 §6) and this constant did not follow it for a day: +// the canon moved first and the gate stood red the whole time, which is the reverse of the order the +// paragraph above ratifies. The number is brought to the canon here rather than the other way round, +// because the canon is the one that is right — the wire this build serves is the 0.13.1 shape. +const ContractVersion = "0.13.1" // Capabilities is what this deployment can do: one flat document, the same for every account. type Capabilities struct {