22 KiB
⚠ ЭТО ХЕНДОФФ-ПРОМТ ПЛАТФОРМЕННОЙ СЕССИИ. Он живой и исполняется. Прочитай
CLAUDE.md, потом этот файл целиком, потом — его карту чтения (§3) и больше ничего, пока не появится вопрос. ⛔ ПАК ПАРНЫЙ. Твоя половина не лендится в одиночку: движковая делается параллельно другой сессией, и обе принимаются вместе (D39.234п.2). Что это значит практически — §2.
Пак платформы: «ДВЕ ОСТАНОВКИ»
1. Какая проблема и что решит твой результат
Пользователь, остановивший перевод книги, сегодня рвёт всё, что летит к провайдерам прямо сейчас:
оплаченные генерации обрываются, движок списывает за них оценку и на продолжении делает их заново.
Владелец 10.09 ратифицировал форму (D39.234 п.1): остановок две — мягкая (новое не начинаем,
начатое доигрываем, ничего не рвём) и жёсткая (сегодняшнее поведение). Первое нажатие — мягкая, второе
— жёсткая.
Твоя зона держит три из четырёх вещей, без которых это не заработает, и одну, которую сама же просила.
⚠ Твой код уже НАПИСАН так, будто мягкая остановка есть. platform/internal/runner/runner.go:56-58
— «the engine … finishes the in-flight chunk before exiting»; :163-164 — «SIGTERM to the engine alone
so it can finish the chunk it is paying for». Сегодня это ЛОЖНО, движок рвёт. После пака станет правдой.
⇒ комментарии не «чинить под дерево», а сверить с тем, что реально построишь.
Носители в трекере (единый бэклог, docs/BACKLOG.md): строки 381 (две остановки, с твоими
адресами) и 382 (пометка «оценка» не едет по шву). Зонные строки заводи по своим правилам.
2. Зона и git
Твоя зона — platform/. backend/ правит движковая сессия, docs/ — оркестратор. Коммитит
только оркестратор. Итоги, вопросы и расхождения — ЗОННЫЙ журнал platform/docs/platform-PROGRESS.md
(в docs/PROGRESS.md платформа не пишет).
⛔ Пара. Форму кадра событий («мягко или жёстко» + счёт и сумма оценочных строк) объявляет ДВИЖКОВАЯ половина. Ты её потребитель. Не проектируй кадр в одиночку: если тебе нужно поле, которого движок не объявил, — это вопрос оркестратору, он сведёт обе половины. Читай пинг №23 в своём журнале: там четыре адреса, снятые оркестратором, и они — вход в эту работу.
3. Карта чтения — ЗАКОН, дальше только по её ссылкам
platform/docs/platform-PROGRESS.md— пинг оркестратора №23 (10.09) сверху: четыре адреса и что с каждым делать.platform/internal/runner/runner.go— целиком:stopGrace(~59), свойства юнита (~160–170),Stop(~194).platform/internal/runs/reconcile.go— как остановка сводится с исходом:StopRequestedAt(~558, ~575, ~591),finishStopped(~678),stoppedOnRequest/interruptedBySomeoneElse(~1018, ~1112–1134), и комментарий ~1534.platform/internal/ingest/exit.go— словарь кодов выхода,ExitStopped = 5(~31–34).docs/architecture/14-api-contract/—/runs/{runId}/stop(openapi.yaml:754+) и его описание в README контракта.
Тела нот: docs/architecture/05-decisions-log.md, номер грепается ^## D<номер>; статус одним хопом —
docs/architecture/05-decisions-index.md. ⚠ ПОДНОМЕР (D39.234 п.1) своего тела не имеет — это ПУНКТ
внутри ## D39.234.
4. Состав и разметка свободы
4.1 ⛔ ГРЕЙС МЕНЬШЕ ЗАКОННОГО ОЖИДАНИЯ — это первое, и без него пак вреден
platform/internal/runner/runner.go:59 — stopGrace = 10 * time.Minute (600 с), он же уезжает в
TimeoutStopSec свойством юнита. Законное ожидание ОДНОГО вызова у движка — до attempt_max_s
1240 с (backend/configs/models.yaml, deepseek), и владелец это ожидание ратифицировал
(D39.230 п.3; потолок подтверждён D39.234 п.1б — «~20 минут приемлемо»).
⇒ в ту секунду, когда SIGTERM станет мягким, systemd начнёт убивать ЗАКОННОЕ ожидание. А SIGKILL не
оставляет ни пометки cancelled, ни сеттла оценки: деньги у провайдера списаны, следа в леджере нет —
то есть ровно та потеря, ради устранения которой пак и делается.
Делай РОВНО так: грейс обязан покрывать потолок ожидания ПЛЮС время жёсткой фазы. Входы формулы
называю я: max(attempt_max_s) по всем провайдерам backend/configs/models.yaml (сегодня 1240 с) +
время жёсткой фазы + запас. Само число выбери сам и обоснуй его в отчёте арифметикой, а не круглостью: откуда взял потолок, сколько заложил на жёсткую
фазу, что будет, если провайдер сменит attempt_max_s. ⚠ Отдельно скажи, как число не протухнет: сегодня
оно живёт константой в твоей зоне, а зависит от конфига в чужой.
4.2 mode у /stop — делай РОВНО так по существу, форму согласуй
Сегодня у продукта один глагол: POST /runs/{runId}/stop (docs/architecture/14-api-contract/openapi.yaml:754),
и он ведёт к systemctl --user stop (runner.go:194), то есть к ОДНОМУ SIGTERM.
После пака один SIGTERM = мягкая остановка. Значит продукту нужен способ сказать «останови СЕЙЧАС».
⇒ у /stop появляется режим, и форму я называю сразу, чтобы не гонять её третий круг: mode: soft|hard,
дефолт soft. Дефолт мягкий — потому что пользователь, который просто нажал «стоп», не должен терять
деньги. ⚠ Контракт — ратифицированный носитель; правку контракта вносит оркестратор, не твоя рука:
пришли точную формулировку описания в отчёте, я её ратифицирую минором.
4.3 Путь второго сигнала — делай РОВНО так
Жёсткая остановка = ВТОРОЙ сигнал движку. systemctl stop для этого не годится: он уже потрачен на
первый и запустит свой грейс.
Форма, которую рекомендует консилиум: systemctl kill --signal=SIGTERM по тому же юниту (юнит не
трогая), то есть просто ещё один SIGTERM процессу. Проверь это сам на своей машине — рекомендация не
замер.
⛔ Кто жмёт второй раз — решено, и в пак идёт ТОЛЬКО (а): пользователь, явным «останови сейчас».
⚠ Испр. до выдачи: прежняя редакция выносила это сессии как развилку и приводила довод за (б) —
автоэскалацию платформой. Довод снят и вот почему: (б) означает, что платформа САМА решает оборвать
оплаченные вызовы, списать за них оценки и купить их заново на резюме — без слова пользователя. Это
противоречит канону владельца («деньги видимы с первого вызова, явное согласие на пере-оплату») и его
же слову «готов ждать» (D39.230 п.3). Для живучести (б) и не нужна: мягкое ожидание движка само
ограничено дедлайном попытки, а SIGKILL по грейсу — последний рубеж. Если ты считаешь иначе — это
вопрос ВЛАДЕЛЬЦУ через оркестратора, а не решение сессии.
4.4 Читать счёт и сумму оценочных строк — делай РОВНО так
⭐ Это то, что твоя же зона просила, и оно наконец приедет.
platform/internal/pgstore/credits.go:235 — «publish the count and sum of estimated-price rows beside
committed_usd, which is what would let this side say "at most Y"».
Движок эти числа считает и печатает (backend/internal/pipeline/status.go:321 — estimated_rows /
estimated_usd), но по шву они не едут: в кадрах backend/internal/runevents/runevents.go слова
estimated 0 хитов (контроль: committed в том же файле — 6; денежный кадр несёт один
committed_micro_usd, ~244). С твоей стороны читателя тоже нет: в platform/internal вне тестов
estimated — 2 хита, оба комментарии (контроль: committed_micro — 1 хит; go-файлов вне тестов
прибор прочёл 81).
Движковая половина везёт их в кадр. Твоя — прочитать и довести до пользователя. Зачем это нужно
владельцу дословно: он разрешил списывать по ОЦЕНКЕ при УСЛОВИИ, что пометка стоит (D39.230 п.1) —
условие сегодня исполнено на одном канале из двух и потребителя не имеет. Пользователь должен видеть не
только «списано X», но и «из них оценка Y».
4.5 Протухшая проза про движок — почини, раз ты здесь
platform/internal/runs/reconcile.go:1534 и platform/internal/pgstore/runs.go:320 пишут «the engine
catches SIGTERM and exits 1». Движок на пойманном сигнале отдаёт 5 (backend/cmd/tmctl/main.go:100),
и твой собственный код это знает (platform/internal/ingest/exit.go:34 = ExitStopped = 5,
reconcile.go:1134 сверяется именно с ним). Врут только комментарии — но их цитируют доки, и ложь
уезжает дальше. Тот же текст — в platform/internal/runs/control_test.go:201 (⚠ адрес испр. до выдачи: файла
platform/internal/runner/control_test.go не существует).
4.7 ⛔ САМОЕ ДОРОГОЕ: Stop БЛОКИРУЕТСЯ НА ВЕСЬ ГРЕЙС — замерено исполнением
Runner.Stop зовёт systemctl --user stop без --no-block (platform/internal/runner/runner.go:195),
а runCommand собственного тайм-аута не имеет (:106-112). Пока стоп ЖЁСТКИЙ, юнит умирает за секунды и
этого не видно. С мягким SIGTERM stop будет ждать столько, сколько живёт движок.
Замер (пробник на транзиентном юните, игнорирующем SIGTERM, TimeoutStopSec=15):
systemctl --user stop вернулся через 15,21 с — то есть по SIGKILL; stop --no-block — через
0,019 с, юнит в deactivating.
⇒ два прямых следствия, оба ломают продукт в первый же день:
- в HTTP-глаголе
/stopприWriteTimeout: 30 s(platform/cmd/tmplatformd/main.go, грепWriteTimeout) пользователь НЕ получает202, который контракт обещает (docs/architecture/14-api-contract/openapi.yaml, грепThe202does not mean the run has stopped); - свип реконсайлера (
platform/internal/runs/reconcile.go, вызовыStop) встанет до двадцати минут на одном прогоне.
Делай РОВНО так: --no-block. Реконсайлер и так пере-выдаёт стоп на следующем проходе — это написано
в комментарии над его Stop. ⚠ И пере-сними мой замер своей рукой, прежде чем опираться: он снят не на
вашем юните.
⭐ Заодно закрыт вопрос второго сигнала (§4.3): systemctl kill --signal=… на юните в состоянии
deactivating ДОХОДИТ до процесса, состояние не меняется. Рекомендация — --kill-whom=main.
4.8 Ещё два места, названные ревью — делай РОВНО так
(а) Dev-супервизор. platform/internal/ingest/supervisor.go шлёт SIGINT и держит WaitDelay 30 с
(греп WaitDelay). В dev-режиме мягкая остановка будет убита через полминуты. Либо поднять, либо явно
объявить «dev не поддерживает мягкую остановку» — но не молчать.
(б) ЗАПАСНОЙ КАНАЛ для чисел. У прогона, убитого по грейсу, кадра finished НЕТ ВООБЩЕ (его
отсутствие само по себе значимо — см. шапку Finished в backend/internal/runevents/runevents.go). При
этом status --json движка несёт estimated_rows/estimated_usd всегда. ⇒ платформа обязана читать ОБА
канала, и это твоя половина, а не движковая.
4.6 Чего в паке НЕТ — не делай
- Движковая половина — чужая зона. Ты не правишь
backend/вообще, включая тесты. - Коды выхода — заморожены: оба стопа остаются 5. Различие «мягко / оборвано» приезжает ЧИСЛАМИ
в кадре, а не новым кодом и не новым значением
outcome(довод — в комментарии к полюFinished.Volume,backend/internal/runevents/runevents.go, прочитай его прежде чем спорить). - Правка контракта своей рукой — через оркестратора (§4.2).
- Квоты и фри-тир — их нет и не проектируется (
D39.176п.1).
5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается
Норма проекта и решение владельца. Самоотчёт «проверено» без исполнения здесь регулярно оказывался ложным.
- Baseline СВОИМ прогоном ДО первой правки — зелень зоны целиком, скипы названы поимённо.
- ⛔ Отрицательный замер обязан ДОКАЗАТЬ, что спросил существующее: рядом с нулём ПЕЧАТАЕТСЯ контрольная величина. Не «проверено с контролем», а число.
- Грейс проверяется ИСПОЛНЕНИЕМ, а не чтением конфига. Сценарий, который обязан быть в отчёте: юнит с новым грейсом, процесс, который не выходит N секунд, — и что реально сделал systemd. ⚠ Это единственная часть пака, где ошибка молча уничтожает деньги пользователя.
- Второй сигнал — тоже исполнением: что именно приходит процессу и в каком порядке.
- Адверсариальный проход по СВОЕЙ готовой работе, отдельным заходом. Направление — что здесь
уязвимо: (а) гонка «пользователь нажал второй раз, пока идёт первый»; (б) остановка юнита, который
уже мёртв (у
Stopдля этого есть намеренная ветка — не сломай её); (в) реконсиляция: не читается ли мягко остановленный прогон как «упал» или как «дошёл до конца книги»; (г) идемпотентность/stop.
6. Записка-план ДО первой правки
В зонный журнал: порядок работы и почему такой. Рекомендованный: §4.1 грейс (без него остальное вредно) → §4.3 второй сигнал → §4.2 режим → §4.4 чтение чисел → §4.5 проза.
7. Заявление = команда
Любое «сделано / проверено» сопровождается КОМАНДОЙ и её выводом. Утверждение без команды не считается.
8. Эхо-протокол старта
Первым сообщением верни: (а) одной фразой — что произойдёт, если заландить твою половину без движковой, и что если наоборот; (б) какое число ты считаешь самым опасным в этом паке; (в) что здесь неверно или недоказано — право сказать это у тебя есть.
9. Канал вопросов и право сказать «этого делать не надо»
Вопрос — секцией в platform/docs/platform-PROGRESS.md. ⚠ Слово, живущее только в переписке, умирает
вместе с сессией — этой смене это стоило трёх потерянных диспозиций (D39.231 п.1). Всё, что должно
пережить твою сессию, пишется в репозиторий.
10. Что НЕ удалось — обязательная секция отчёта
Пустой она не бывает.
11. Критерий завершённости
- Грейс покрывает законное ожидание движка, число обосновано арифметикой и проверено исполнением.
- У продукта есть обе остановки (
mode: soft|hard, дефолтsoft); путь второго сигнала работает и показан исполнением. 2-бис.Stopне блокируется на грейс — показано исполнением, своим замером, а не моим (§4.7). - Мягко остановленный прогон не читается реконсиляцией ни как падение, ни как дочитанная книга.
- Счёт и сумма оценочных строк доходят до пользователя.
- Протухшая проза про движок исправлена.
- Зелень зоны целиком, скипы названы; отчёт: что не удалось, что изменил против пака и почему.
Деньги
Пак $0: платных прогонов не требует и не разрешает.
Отчёт
platform/docs/platform-PROGRESS.md. Числа — командами. Ничего не коммить.