WEB-094 · Задача · Инфраструктура · web
[ARCH-P0.1] Единый admission control для дорогих путей (chat-global, factcheck, deep research, video submit, podcast)
Закрыт
P0 · горит
ведёт: backup-opus
Суть
Сейчас защита route-local и process-local: `src/middleware/rateLimit.ts:12-25` — лимит живёт в памяти процесса, ключуется по IP, исчезает при рестарте и умножается на число инстансов. Дорогие пути без общего бюджета: `src/app/api/chat-global/route.ts:2288-2368`, `src/lib/podcast/api.ts:1361-1620`, `src/app/api/video/pipeline/run/route.ts:1128-1149`.
СДЕЛАТЬ: обязательный withExpensiveOperationGuard — per-user, per-IP abuse cap, per-provider и глобальный in-flight cap, единый 429 + Retry-After; policy registry вместо чисел в роутах. Счётчики — в Postgres/Redis, process Map глобальным не считать.
ОЦЕНКА: малая кровь, 2-3 дня. Эпик: см. родительский.
Источник: аудит архитектуры (волна archaudit, sol/xhigh, 21.08), отчёт M4 `/Users/milamarty/work/ARCHAUDIT-REPORT.md`, снимок кода `a9b2440d`. Вердикт аудита: годимся для роста 25→1000 после короткого P0/P1-hardening; переписывать не надо.
**Документы эпика (обогащение по требованию owner 21.08):**
- Первичный аудит (ОТДАН РЕВИЗОРУ owner-ом): ~/Downloads/АУДИТ-АРХИТЕКТУРЫ-20260821.md на ноутбуке; оригинал M4 /Users/milamarty/work/ARCHAUDIT-REPORT.md (365 строк, снимок кода a9b2440d, каждое утверждение со ссылкой file:line).
- Раздел с полной таблицей чеклиста — §1; детальный разбор базового слоя — §2; 10 массовых запросов и минимальный индексный пакет — §3; распределённость и гонки — §4; observability — §5; выкатка/rollback/health — §6; ТОП-5 рисков — §7; ЧТО НЕ ДЕЛАТЬ — §8; backlog по риску — §9; rollout по когортам — §10.
- Смежные документы: план переезда ~/Downloads/prodplan-20260810/PROD-MIGRATION-PLAN-R4.md (Intel), аудит миграции M4 MIGAUDIT-REPORT.md, репетиция переезда A1 ~/waves/GATE514-REPORT.md (измеренные RTO), эпик миграции WEB-282.
- Ветка работ: archp0 (worktree A1 wt-archp0, от посадки-30 23e0b944).
## 21.08 ~12:40 КАРКАС ГОТОВ: коммит b58443e6 — общий фреймворк лимитов + подключён к одному пути как образец. ОСТАЛОСЬ: подключить остальные дорогие пути (chat-global, factcheck, deep research, podcast) и ingress-уровень, чтобы новый роут не мог «забыть» гард (замечание ревизора).
Ветка archp0/fcsrc2/migfix от посадки-30 (23e0b944), worktree на A1. Отчёты: ~/waves/ARCHP0-REPORT.md, FCSRC2-REPORT.md, MIGFIX-REPORT.md.
## 21.08 — ВЫЕХАЛО НА ПРОД
**Посадка 30 состоялась 21.08 03:53 IST.** serving = `v4-504af77f`, релиз `arm64-504af77f-20260821T025311Z`, артефакт sha `a0afc41415db60377aeb4cf399d91b98a4c407217fc8839b0b1369a2542af362`, аттестация v18. Проверено после переключения: `active`, `NRestarts=0`, `paidReady=true`, `posture=enforce_ready`, `mode=enforce`, `failures=[]`, фатальных строк boot-контракта **0**, секрет сторожа файл≡процесс (`cbabe4a2b16008e3`), nft переприменены, канон проштампован (`CANON.json: serving=504af77f marker=v4-504af77f`).
Проверка байтов релиза и живая проба выполнены после переключения; подробный трейл посадки — в WEB-282.
## 23.08 — вошёл в посадку, ждёт живой приёмки [эпик WEB-314]
- Груз в проде: 26116cff (посадка 31, 22.08 20:53 UTC, serving); артефакт a3c1c9d6…b595; paidReady=true.
- Метод приёмки: живое: admission control на параллельных дорогих запросах; server resolver вне client graph.
- Порядок: ancestry+байты в артефакте → live-проба → скрин/raw run id в тикет; правила и доки-основания в WEB-314.
## 23.08 evidence-pass (l3031-evidence)
- Audit: GET before PATCH found no exact 23.08 evidence-pass section; existing body was preserved and this is the single appended section.
- Ancestry: git merge-base --is-ancestor b58443e6 26116cff rc=0; exact source=26116cff032ad8f015e1e2d9cba302e1e8f7ec27.
- Bytes: shared Postgres-backed limiter, common 429/Retry-After response, and one wired adopter (video:submit) are present. Exact source explicitly does not prove chat-global/factcheck/deep-research/podcast all adopted it; source/ancestry SHA-256=5fd780dc8d9f4823ea318dfcbac27c76a8333d75e0a68a084073e4add1946db2.
- Live: no authenticated QA session (callback HTTP 302/session=null; login SHA-256=3020bdf182128451795610e09688faefe24211460f13f84648c1991ad7e29a55). No 3–5 expensive requests were sent and no runId/admission headers claimed; this also avoids an unbounded paid probe.
- Verdict proposal: NEEDS_LIVE_HAND.
## 23.08 09:40 UTC — ЖИВАЯ приёмка (волна l3031live, M4, рабочий логин 200/200/200). NEEDS-FIX (живой дефект): 3 параллельных дорогих chat-global → admission СРАБОТАЛ (503 accounting_unhealthy + 2×500 concurrency_limit), но БЕЗ Retry-After/queue/admission-заголовков и 500 вместо 429. Требует корректного admission-протокола.
## 2026-08-23 17:20 UTC — фикс-волна запущена (A1 web094, Luna xhigh)
Скоуп: withExpensiveOperationGuard (per-user/per-IP/per-provider/global in-flight, policy registry, счётчики в Postgres), единый 429+Retry-After вместо 500 на chat-global/podcast/video, 4 негатива, additive-миграция если нужна. Не конфликтует с биллинг-гейтом WEB-322 (admission выше биллинга). Ожидаю WEB094_FIX_READY — кандидат посадки 34/35.
## 2026-08-23 18:00 UTC — ФИКС ГОТОВ (WEB094_FIX_READY), поедет ОТДЕЛЬНОЙ посадкой 35 (несёт миграцию)
Коммит b015c91d3 (от 00c402c), патч спасён на Интел. withExpensiveOperationGuard: durable Postgres-счётчики (RateLimitCounter fixed-window per-user/per-IP) + durable lease (провайдерные и глобальный in-flight), транзакция под advisory lock ($executeRaw — урок 311b учтён), L1-память НИКОГДА не авторитет. Отказ = 429+Retry-After+admission_limit_exceeded/admission_queue_full; сбой стора/битая политика = машинный 503 fail-closed, не 500. Обвязаны chat-global (ДО биллинг-обёртки — не маскирует), podcast preview+synthesize (до reservePodcastUsage), video/pipeline/run (lease в finally). ADDITIVE миграция 20260823100000_web094_expensive_admission (standing OK). Тесты 4/4+15/15+podcast 1/1+3/3, admission-tsc rc=0, prisma validate rc=0. Полный tsc rc=2 = ПРЕДСУЩЕСТВУЮЩИЙ baseline 402 диагностик (WEB-281), в затронутых зонах 0. Причина отдельной посадки: миграция требует prisma migrate deploy шага (prod-db-migration-ledger) — не смешиваю с большой 34-й.
## 2026-08-24 02:55 UTC — 🎉 ЗАДЕПЛОЕНО: посадка 35 (v4-8a29afa79, 1-я попытка, ПЯТАЯ за день)
7 грузов; prisma migrate deploy ДО флипа (применены 3 additive: web094 + догнавшие web097/web300); пин-гейт 7/7 с НОВЫМ SPEND_MIGRATION_SET_SHA256=ae1bf466…; paidReady=true; канон проштампован. ЖИВОЙ ПРУФ ГОЛОС-ЗАБОРА: GET /api/realtime unauth → 401 (был 404), /api/telephony → 404 (закрыт как задумано).
## 2026-08-23 BOARDSWEEP: STALE-NOT-DEPLOYED
- Evidence: Canonical has the earlier admission framework/migration, but the ticket's current required fix is the later WEB094_FIX_READY b015c91d3 with all expensive adopters and 429 contract; that candidate is after canonical and the description says it awaits a later landing. buildFixed=26116cff does not prove the post-canonical candidate is serving. No paid burst sent.
- buildFixed: 26116cff; canonical: v4-081ff4fb5 / 081ff4fb5f6110f8001daec7ed910ccef2e395dc.
- Status preserved by sweep: review.
Доказательства
[2026-08-24 ~17:20] Посадка 46 (v4-8b6ed44a6): withExpensiveOperationGuard подключён к factcheck/run и deep-research/run (commit d38864095), route-тесты + общий фокус-прогон 71/71 rc0. На приёмку: негативная проба на живом проде (guard срабатывает).
[2026-08-25 ~14:30] НАХОДКА из платных прогонов (BQA-2+BQA-3): при окне авто-реконсайла accounting (SPEND_RECONCILE_INTERVAL_SEC=600) internal-провайдер фактчека ХАРД-ФЕЙЛИТ платный запрос 503 «AI spending accounting is being repaired, so new paid requests are refused» — без очереди/ретрая. /api/ready/paid при этом зелёный (paidReady=true), т.е. ready-эндпоинт не отражает краткие paid-admission отказы. Воспроизведено дважды за 30 мин. Нужно: admission должен ставить платные в очередь/ретраить через reconcile-окно, а не отдавать 503. Родня: [[WEB-3]] (заблокировал приёмку PDF), [[WEB-320]] фактчек.
[2026-08-25 ~17:50] WEB094_DONE — расследование платного 503 (отчёт A1 /home/ubuntu/waves/WEB094-REPORT.md). КОРЕНЬ: это ОСОЗНАННЫЙ fail-closed денежной безопасности, НЕ забытая очередь. settlementLatch invariant (src/lib/billing/settlementLatch.ts:1-29,98-158): неразрешённый post-dispatch settlement → deny новой owner-paid работы; elapsed time НЕ открывает gate; audited reopen только после durable empty backlog. Отказ рождается в spendGuard.ts:143-183 (accounting_unhealthy→503), internal factcheck идёт через тот же paid accounting gate (не обходит). «Окно reconcile» — это на самом деле unresolved settlement/projection backlog ИЛИ закрытый durable epoch/gate; SPEND_RECONCILE_INTERVAL_SEC=600 — dead-man (stale после ×3=1800с), не 600с отказное окно. ВАРИАНТЫ (риск↓): г) readiness честный относительно effective paid state (сейчас /ready/paid зелёный, а paid 503 — врёт) — очень низкий риск, ничего не списывает; в) Retry-After/retryable metadata — низкий, только UX; б) сократить reconcile-окно после измерений — низкий-средний; а) очередь/ретрай paid — САМЫЙ ВЫСОКИЙ риск (двойной dispatch/debit), НЕ рекомендован без нового audited billing state machine с exactly-once. РЕКОМЕНДАЦИЯ: г→в→(б после замеров), НЕ трогать accounting_unhealthy→503/lock-order/gate semantics/ledger без owner-approval. ЖДЁТ РЕШЕНИЯ OWNER какой вариант. Родня: [[WEB-3]] (заблокировал платную PDF-приёмку), [[WEB-320]].
[2026-08-25 ~07:20] OWNER-РЕШЕНИЕ (tg 06:22): ДА на безопасные фиксы (г честный readiness + в retry-after) И одобрил денежный дизайн очереди («денежный дизайн так же, если надо делайте»). План: (1) СЕЙЧАС безопасные г+в как landable-волна (readiness отражает effective paid state; Retry-After/retryable metadata — БЕЗ изменения accounting_unhealthy→503, без монетарных мутаций); (2) ОТДЕЛЬНО ДИЗАЙН option а (durable paid-job queue, audited billing state machine, exactly-once) — только ДИЗАЙН-документ на ревью, БЕЗ прод-монетарных изменений до owner-приёмки дизайна (constraint: no unapproved paid/prod mutations). Ставлю в очередь на первый свободный слот (сейчас A1=L52 сборка, M4=fcprep2).
[2026-08-25 ~08:00] WEB094FIX_DONE. ЧАСТЬ 1 реализована+закоммичена (ветка l48-salvage @ e857b22, база 446bcbf, БЕЗ монетарных мутаций, fail-closed): (г) честный /api/ready/paid — read-only проекция readPaidAccountingHealth (durable truth+backlog+reconcile dead-man closeGate:false+process-local latch+provider:* gates); в enforce отдаёт paidReady:false+503 с reason/reasons/retryPosture/retryable/retryAfterSeconds, различает durable-unhealthy/reconcile-stale/process-local-latch/provider-scope/database-failure; НЕ reopen, НЕ provision gate rows, НЕ bypass (admission по-прежнему authoritative). (в) Retry-After:5 + retryable:true + retryAfterSeconds:5 на accounting_unhealthy/spend_state_unavailable 503. Тесты: web094PaidHealth 4/4, frozen/source 28, spend guard/reservation 61, W3.8/scan 27, tsc rc0, git diff --check rc0. ГОТОВ К ПОСАДКЕ (батч). ЧАСТЬ 2 ДИЗАЙН-ДОК option а (durable paid-job queue) → /home/ubuntu/waves/WEB094-QUEUE-DESIGN.md + доставлен owner ~/Downloads/WEB094-QUEUE-DESIGN.md: idempotency_key/admission_generation/exactly-once/crash-recovery/two-workers/no time-based gate reopen. ЖДЁТ ОК OWNER на дизайн до любой монетарной реализации.
[2026-08-25 ~10:45] Дизайн платной очереди: v4 sol NOGO (7 денежных блокеров + мета SQL неисполним :vars в DO$$) → v5 закрыл мета-модель (typed PL/pgSQL) но qreview5 NOGO, все 7 блокеров стали PARTIAL (СХОДИТСЯ) → запущена v6 добить 8 остатков+2 опечатки (queue_t0_reserve двоит capacity при повторе; wallet projection zero-row permissive; T3 zero-cap leased без recovery; DLQ success без proof; reaper не revalidate expiry; capability expiry вне signed tuple; correction перезаписывает до сравнения; repair не монотонен). Отдельно: Part1 (e857b22 честный readiness) СОБРАН в артефакте посадки 53. Это КАЧЕСТВО денег, не архив.
[25.08 ~12:25] Part1 (e857b22, честный paid-readiness/retries) В ПРОДЕ посадкой 53 (v4-c3fb44e). Дизайн очереди: v6 дописан (A1 /home/ubuntu/waves/WEB094-QUEUE-DESIGN-v6.md, маркер QDESIGNV6_DONE, «CLOSED по design-only контракту»), запущена независимая sol-ревизия qreview6 (xhigh, отчёт будет в qreview6-sol-report.md + маркер QREVIEW6_GO/NOGO).
[25.08 ~12:45] qreview6 (sol xhigh, независимый) = NOGO: 7 остаточных блокеров — битые END; в PL/pgSQL / нет pinned-схемы, blocker parent без STRICT, poison-abort без исполнимой DLQ-транзакции и REDRIVING→RESOLVED, repair не повторяем поверх применённых consumers (SUCCEEDED из FAILED_ACCOUNTING недостижим), reaper создаёт durable incident до revalidate, late-receipt correction для SETTLED_MAX недостижим (NULL=receipt_hash → 0 rows), T3 expired-capability settlement. Отчёт: A1 /home/ubuntu/waves/qreview6-sol-report.md. Петля предметная (качество денег, не архив). Запущен qdesign-v7 (luna xhigh) таргетно по 7 пунктам (брифом требую разбор сценариев A-G и полный DDL в документе). Вопрос о лимите раундов задан owner (TG).
[25.08 ~13:10] qdesign-v7 DONE: автор закрыл 7 блокеров qreview6 и ВПЕРВЫЕ исполнил SQL в чистой PostgreSQL-схеме (24 функции, 23 body с корректным END;, smoke T0/DLQ-redrive/T3/T5/projection/correction/repair). Файл A1 /home/ubuntu/waves/WEB094-QUEUE-DESIGN-v7.md (126КБ). Запущен qreview7 (sol xhigh): обязан САМ прогнать DDL в чистой схеме + 3 НОВЫХ контрпримера (новые классы). Маркер QREVIEW7_GO/NOGO.
[25.08 ~14:00] qreview7 = NOGO (отчёт qreview7-sol-report.md: часть старого закрыта, найдено 3 НОВЫХ класса: settled-hold не освобождает reserved cache (happy-path!), correction ломает committed projection, null-reservation redrive ложно RESOLVED; crash-класс между DLQ insert и commit — ПОГАШЕН, atomicity ok). По решению owner («v7 последний дизайн-раунд, как ты говориш»): ДИЗАЙН ЗАМОРОЖЕН КАК КОНТРАКТ (WEB094-QUEUE-DESIGN-v7.md), остатки нарезаны в имплементационные тикеты с приёмкой: WEB-357, WEB-358, WEB-359, WEB-360, WEB-361. Дизайн-петля ЗАВЕРШЕНА.
📎 Трейл дизайн-петли: v4..v7 файлы WEB094-QUEUE-DESIGN-v*.md + ревизии qreview4..7-sol-report.md (все на A1 /home/ubuntu/waves/); контракт заморожен: docs/billing/QUEUE-CONTRACT-V7.md (в проде 54); имплементация: WEB-357..361.
[25.08 ~19:15Z] Независимая приёмка qaccept (sol, СВОЯ чистая БД + клоны на каждый smoke): 5/5 QIMPL-фиксов подтверждены в их сценариях, вкл. crash-репро (pg_terminate_backend) и SERIALIZABLE-гонки (40001+retry). НО NOGO по 2 НОВЫМ блокерам: (1) 0/26 функций с pinned search_path (pg_proc.proconfig) — нормативный вызов из чистой сессии не работает; (2) repeat correction QIMPL-4 сравнивает immutable receipt с mutable DailySpendCounter — чужой T5 в scope ломает repeat (committed-response-loss класс). Запущена qimpl6 (от qimpl4) ровно на два блокера → потом ре-приёмка. 📎 QACCEPT-REPORT.md + qaccept-evidence/ на A1.
[25.08 ~20:20Z] qimpl6 СДЕЛАН (fb6a9a7): 26/26 функций pinned search_path=web094_v7,pg_temp; correction repeat читает только immutable (чужой T5 в scope больше не ломает). Запущена ре-приёмка qaccept2 (тот же независимый приёмщик, свои чистые БД, оба репро дословно + регресс QIMPL-1..5 + 1 новый класс). 📎 QIMPL6-REPORT.md, ветка qimpl6.
[25.08 ~21:25Z] 🏁 QACCEPT2 = GO (независимая ре-приёмка, свои чистые БД): 26/26 pinned search_path, нормативный T3 из новой сессии, чужой T5 не ломает correction repeat, регресс QIMPL-1..5 + новый temporal-класс — всё зелёное. САГА ОЧЕРЕДИ ЗАКРЫТА: дизайн v4→v7 (заморожен) → QIMPL-1..6 → приёмка GO. Итоговая ветка qimpl6@fb6a9a7 — кандидат посадки 55 (SQL спящий, применение на прод отдельным решением). 📎 QACCEPT2-REPORT.md.
[25.08 ~16:30Z] В ПРОДЕ посадкой 55 (релиз arm64-l60, v4-a453b16, Gate D ON, paidReady+attest ok). queue-v7 SQL (qimpl6) спящий в проде.
[2026-08-26 00:22Z] web094 ДИЗАЙН/АНАЛИЗ (read-only, монетарное не менялось): текущее состояние admission — 2 слоя paid admission (llmBilling preflight + withExpensiveOperationGuard rate/capacity 429/Retry-After в src/lib/admission/index.ts). withExpensiveOperationGuard лимитирует rate/in-flight, но НЕ сверяет accounting-reconcile-state. /api/ready/paid НЕ является admission lock. Для безопасной 'парковки' платного fact-check нужен НОВЫЙ durable job/idempotency контракт с повторной проверкой accounting-truth перед dispatch (текущий 503 сохраняет денежную безопасность ценой availability). → следующий шаг: имплементация durable-job контракта (отдельная волна по этому дизайну). Отчёт A1:WEB094-REPORT.md.
[2026-08-26 01:18Z] Автор web094impl @03ef2f7: durable admission/idempotency для всех 4 дорогих путей (chat-global/factcheck/deep-research/video) с fail-closed + повторная проверка accounting-truth перед dispatch (безопасная парковка вместо жёсткого 503). Тесты 11 passed, check:client-boundary rc=0, Prisma validate + scoped TS OK. -> review на НЕЗАВИСИМУЮ МОНЕТАРНУЮ приёмку (негативы: accounting-unhealthy->park не dispatch, обход невозможен, идемпотентный redrive).
[2026-08-26 00:55Z] ПРИНЯТ acc094 (независимая монетарная): GO — durable admission/idempotency для 4 дорогих путей, accounting-unhealthy->park не dispatch, обход невозможен, идемпотентный redrive. -> done.
Починено в
a453b16
Лента
2026-08-23T08:30:21.850Z · codex-l3031-liveL3031 LIVE NEEDS-FIX: 3 параллельных дорогих запроса дали 503 accounting_unhealthy и 2×500 concurrency_limit — guard сработал, но Retry-After/queue/admission headers отсутствуют, guard возвращает 500. Raw: qa-screens/l3031-web060-chat-admission.json. Предлагаемый статус: нужна доработка протокола.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-094","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-26T00:51:44.301Z