WEB-477 · Дефект · — · web
P11 (журнал платных операций) ломает чат: chat-global требует x-idempotency-key, веб-клиент и батарея его не шлют → 400 всем; на l91 флаг откачен
Закрыт
P1 · важно
ведёт: Fable
Суть
Факт (прод l91 9ad68bc2, 02.09 14:15–14:20 A1): после посадки l91 с PAID_OP_RECORD_ENABLED=1 (карточка P11 runbook WEB-093, owner GO 12:47) батарея ролей r31 = 0/50: все POST /api/chat-global → HTTP 400 за ~120 мс, тело пустое у клиента; nginx: 47×400 подряд, 200 после посадки — ни одного. Корень: src/app/api/chat-global/route.ts:2960–2962 — при включённом P11 `resolvePaidOperationClientKey(req)` читает только заголовки x-idempotency-key / idempotency-key (src/lib/billing/paidOperationRecord.ts:110–111) и при отсутствии возвращает paidOperationIdempotencyKeyRequiredResponse() (400, code paid_operation_idempotency_key_required). Веб-клиент чата ключ НЕ шлёт (в src/components|lib|store его шлют только factcheck panel, realtime drivers, telephony, podcast); батарея ролей — тоже нет. Итог: с P11 чат недоступен всем браузерным пользователям и внешним клиентам (iOS). Runbook P11 говорит «wrapper requires a client idempotency key; missing keys are rejected before provider dispatch» — но не говорит, что основной клиент ключ не шлёт.
Мера (14:21 A1): PAID_OP_RECORD_ENABLED=0 в /home/ubuntu/prod/shared/.env.local (бэкап .env.local.bak-p11off-*), restart nc-a1 + SIP-стек; ready/paid TRUE; доказательство чата — батарея C1 (r31b). l91 остаётся; P11 OFF до фикса.
Фикс (волна, l92): (1) веб-клиент чата: стабильный x-idempotency-key на каждое сообщение (uuid при отправке, переиспользуется при ретрае того же сообщения) — найти единый fetch-слой чата (store/useStore или lib/chat client) и подключить; (2) сервер: для аутентифицированных браузерных сессий без ключа — НЕ 400, а fallback-ключ из (conversationId, messageId/contentHash, userId) с пометкой keySource=derived в записи (иначе внешние/старые клиенты ломаются при каждом включении); строгий 400 — только для API-клиентов с явным флагом; (3) батарея ролей (roles-prod/live/battery-v10.mjs) и iOS-клиент: слать ключ; (4) тест-негатив: чат без ключа при P11=1 → 200 с derived-ключом, дубль с тем же ключом → replay; (5) в ENT093ACTIVATE-RUNBOOK карточку P11 дополнить предусловием «клиенты шлют ключ / derived fallback».
Урок (память landing-must-probe-paid-paths-not-just-http): /ready/paid=TRUE и dry-run не ловят 400 на чате — после каждого flip прогонять хотя бы один платный чат (батарея C1 или curl с QA-сессией) ДО объявления посадки зелёной.
Лента
2026-09-02T14:21:45.348Z · Fable[A1 14:26 Фабл] Доказательство восстановления: батарея ролей C1 (r31b) после PAID_OP_RECORD_ENABLED=0 — HTTP 200 везде, 5/7 (C1-2 и C1-5 теперь проходят: судья цитат WEB-474 работает; C1-3/C1-6 — retrieval-классы, волна 1224). Фикс-волна 1234-web477idemkey (A1, база l91): клиентский x-idempotency-key + derived-fallback на сервере + тесты; P11 → l92.
2026-09-02T15:09:43.035Z · Fable[A1 15:15 Фабл] Волна 1234-web477idemkey (A1) сдана: d5ba8bf7f2f1568356b15d951cd9c576625f70e4 (ветка web477idemkey-1234, база l91): веб-чат шлёт x-idempotency-key = UUID сообщения (ChatInterface/GlobalChatModal/SearchMode/EditorModal/useVoiceQuery; retry/reconnect/SSE replay — тот же UUID; server action через ChatOptions.idempotencyKey); сервер при cookie-сессии без ключа — derived sha256(userId|conversationId|messageId|contentHash) с PaidOperationRecord.keySource=derived (новая колонка, миграция 20260902143000_web477_paid_operation_key_source, compat 172/172), Bearer/API-key без ключа или PAID_OP_RECORD_STRICT_CLIENT_KEY=1 → 400; P11=0 — как раньше. Проверки: scoped tsc, eslint, client-boundary, migration gates PASS. Приёмка 1249-accweb477idemkey на M4 (real PG: migrate deploy, replay/derived/strict матрица). Линия на A2/M4. После GO → l92 → P11 снова ON.
2026-09-02T15:31:35.869Z · Fable[A1 15:32 Фабл] Приёмка 1249-accweb477idemkey (M4, real PG17+pgvector): **NO-GO** — (1) чистый `prisma migrate deploy` 172 миграций падает SQLSTATE 25001 на уже применённых на проде P11-миграциях 20260831011000/20260831021000 (`CREATE INDEX CONCURRENTLY` внутри транзакции Prisma; то же нашла приёмка 1195 WEB-468) — новая миграция keySource сама additive и чистая; (2) P11 durable record/replay проведён только в chat-global — notebook chat, source chat и server action chatWithSources лишь передают ключ, без withPaidOperationRecord/derived/strict/replay. Корректировка 1256-web477idemkey2 (M4, real PG): миграции deploy-able для чистых установок (CONCURRENTLY → CREATE INDEX или вынос в операторскую команду; манифест совместимости регенерируется), единый helper P11 на 4 поверхностях + тесты. P11 остаётся OFF до l92.
2026-09-02T16:02:43.958Z · Fable[A1 16:03 Фабл] Корректировка после NO-GO 1249 сдана: волна 1256-web477idemkey2 (M4, реальный PG 17), коммит 36de2b4f (ветка web477idemkey2-1256 = d5ba8bf7 + 36de2b4f, база l91-candidate). Что сделано: обе P11-миграции (20260831011000, 20260831021000) — CREATE INDEX CONCURRENTLY заменён на транзакционный CREATE INDEX (чистый migrate deploy 172/172 проходит); единый idempotency-wiring для четырёх поверхностей (chat-global, notebook REST, source REST, chatWithSources): ключ выводится сервером, клиентский x-idempotency-key необязателен, replay без второго вызова провайдера, невалидный ключ → 400 до провайдера; манифесты перегенерированы, activation migration set = 09cf87a2… (172). Отчёт автора: M4 /Users/milamarty/waves/WEB477IDEMKEY2-REPORT.md. Дальше: приёмка 1258-accweb477idemkey2 (M4, реальный PG, другой verifier; ключевой риск — изменённые файлы УЖЕ применённых на проде миграций: проверяется сценарий «леджер прода + deploy дерева автора») → cherry-pick в l92-candidate → посадка l92 с новым SPEND_MIGRATION_SET_SHA256 из аттестации → PAID_OP_RECORD_ENABLED=1 → батарея C1 и запрос без заголовка → затем P01. Бандл: A1 /home/ubuntu/mirrors/lines/web477idemkey2-1256.bundle; A2 nc-build ref web477idemkey2-1256/head.
2026-09-02T16:35:27.189Z · Fable[A1 16:40 Фабл] Приёмка 1258-accweb477idemkey2 = NO-GO (M4 /Users/milamarty/waves/ACCWEB477IDEMKEY2-REPORT.md). Прошло: чистый migrate deploy 172/172 без 25001, сценарий «леджер прода + deploy дерева автора» (No pending migrations, без ошибок checksum), derived-ключ без заголовка, replay без второго вызова провайдера, гонка. Блокер (один, воспроизводимый): resolvePaidOperationClientKey отдаёт null и для отсутствующего, и для невалидного заголовка (пустой/257 символов), а resolveChatPaidOperationKey после null строит derived → cookie-запрос с невалидным x-idempotency-key НЕ получает 400 и доходит до withPaidOperationRecord (все четыре поверхности). → волна 1264-web477idemkey3 (M4, реальный PG): различимый результат резолвера missing|invalid|client|derived; invalid → 400 с причиной до провайдера и без списания; табличные тесты резолвера + HTTP на четырёх поверхностях; манифесты без изменений (09cf87a2…). После GO → l92 → PAID_OP_RECORD_ENABLED=1 → батарея C1 → P01.
2026-09-02T17:17:20.339Z · Fable[A1 17:35 Фабл] Корректировка после NO-GO 1258 сдана: волна 1264-web477idemkey3 (M4, реальный PG), коммит cf0200ef (ветка web477idemkey3-1264 = 1256 + cf0200ef, база l91-candidate). Заявлено: резолвер различает missing|invalid|client|derived; invalid (пустой/257+/недопустимые символы) → 400 с причиной до провайдера и без списания на всех четырёх поверхностях; missing → derived; манифесты без изменений. Отчёт: M4 /Users/milamarty/waves/WEB477IDEMKEY3-REPORT.md. Дальше: приёмка 1283-accweb477idemkey3 (M4, другой verifier: таблица резолвера × режимы, spy провайдера, регресс 1258: чистый deploy, леджер прода, replay, гонка) → GO → cherry-pick в l92-candidate → посадка l92 с SPEND_MIGRATION_SET_SHA256 из аттестации → PAID_OP_RECORD_ENABLED=1 → батарея C1 → P01. Бандл: A1 /home/ubuntu/mirrors/lines/web477idemkey3-1264.bundle; A2 ref web477idemkey3-1264/head.
2026-09-02T18:09:07.983Z · Fable[A1 18:25 Фабл] Приёмка 1283-accweb477idemkey3 (M4, реальный PG): вердикт commit scope = GO (missing≠invalid: 400 code=paid_operation_idempotency_key_invalid до провайдера на 4 поверхностях, без списания; чистый migrate deploy 172/172; сценарий леджера прода; replay/гонка — всё подтверждено), вердикт l92 = NO-GO только из-за отсутствия финальной signed build attestation для этого коммита — она появляется на сборке l92 (l92chain → sign-l92 → land-l92 с SPEND_MIGRATION_SET_SHA256 из аттестации). → cherry-pick web477idemkey3-1264 в l92-candidate (A2); при посадке l92: PAID_OP_RECORD_ENABLED=1 после проверки батареи C1 и запроса с мусорным заголовком → 400; затем P01. Отчёт: M4 /Users/milamarty/waves/ACCWEB477IDEMKEY3-REPORT.md.
2026-09-02T18:39:32.611Z · Fable[A1 18:45 Фабл] ПОСАЖЕНО в прод l92 (9bee5955, 18:37 A1): миграция 20260902143000_web477_paid_operation_key_source применена (schema up to date), SPEND_MIGRATION_SET_SHA256=09cf87a2… из аттестации; PAID_OP_RECORD_ENABLED пока 0. Дальше: батарея C1 на l92 + запрос с мусорным x-idempotency-key → 400 → включение P11=1 (drop-in + restart) → 30 мин наблюдения → P01 (EXPENSIVE_ADMISSION_ENABLED=1, stage p01).
2026-09-02T20:18:39.381Z · Fable[A1 20:17 Фабл] Ловушка посадки: стадия env land-l93.sh перезаписала PAID_OP_RECORD_ENABLED=1 в .env.local (ключ зашит в скрипте) → после flip P11 снова оказался включён на ~3 мин; исправлено: .env.local=0, скрипт=0, restart nc-a1 + ag-sip-native; /proc environ = 0. В следующие land-скрипты: P11 брать из текущего .env.local, не из константы.
2026-09-02T20:49:04.142Z · Fable[A1 20:50 Фабл] Волна 1331-p11fix2 сдана: коммит 5d9ae57a23 (блокнот без источников не списывает; replay byte-identical; access-аудит без PII; проба обновлена). Отчёт: A1 /home/ubuntu/waves/P11FIX2-REPORT.md. Дальше: приёмка 1343-accp11fix2 (A1, в очереди) → l94 → P11=1 на проде + живая проба → P01.
2026-09-02T21:24:47.765Z · Fable[A1 21:45 Фабл] Приёмка 1352-accp11fix2 = NO-GO (отчёт A1 /home/ubuntu/waves/ACCP11FIX2-REPORT.md). Прошло: блокнот без источников не списывает; replay byte-identical; access-аудит без PII; мутационная матрица 3/3. Блокеры: (1) повреждённый responseJson.rawBody в PaidOperationRecord отдаётся replay-ем как 200 (нет digest/проверки целостности снапшота — paidOperationRecord.ts:383-407, 300-326); (2) проба не проверяет «ровно +1 terminal/succeeded» на первом sourced-запросе (две записи → ложный GO). Круг 3 = волна 1370-p11fix3 (A1, Luna xhigh, от 5d9ae57a): digest снапшота + fail-closed (manual_review, провайдер не вызывается), явный путь для legacy-записей, assertion +1 и corrupt-snapshot сценарий в пробе.
2026-09-02T23:05:47.229Z · Fable[A1 02:05 Фабл] Приёмка 1392-accp11fix3 = GO (Sol; digest снапшота + fail-closed replay, legacy-путь, проба с «+1 terminal/succeeded» и corrupt-snapshot). Линия web477idemkey4 (18040022) идёт в l94-candidate. После посадки l94: PAID_OP_RECORD_ENABLED=1 → живая проба p11-live-probe на проде → при GO через 30 мин P01 (land-l94.sh … p01).
2026-09-02T23:32:27.371Z · Fable[A1 03:55 Фабл] В проде l94: web477idemkey4 (P11 круг 3, GO 1392). PAID_OP_RECORD_ENABLED=1 включён 03:50 laptop; живая проба 1412-p11probe2 (A1, Sol) в очереди → при GO через 30 мин P01.
2026-09-03T00:21:31.376Z · Fable[A1 07:50 Фабл] Живая проба 1412-p11probe2 на l94 = NO-GO формально: все функциональные gate PASS (пустой блокнот → 422 без записи; sourced first → ровно +1; replay byte-identical, headers, 0 новых записей; corrupt snapshot → 409); провал только у аудита: в проде не был задан PAID_OP_ACCESS_AUDIT, эмиттер в production выключен → 0 строк за 20 мин, доказать отсутствие чужих 400 нельзя. Действия: P11 временно =0 (07:40), затем PAID_OP_ACCESS_AUDIT=1 + P11=1 в .env.local (07:50), restart nc-a1 + native SIP, прод 200/paid 200; проба 1404b-p11probe3 в голове очереди A1 (новое 20-мин окно). При GO — P01 через 30 мин.
2026-09-03T00:52:52.924Z · Fable[A1 10:05 Фабл] Живая проба 1404b-p11probe3 на проде l94 = GO (Sol): все gate PASS, включая production access-аудит (PAID_OP_ACCESS_AUDIT=1) — за 20-мин окно чужих 400/409 нет. P11 остаётся =1. Следующий шаг: P01 через 30 мин (~10:35) стадией p01 (перенос из land-l92.sh в land-l94.sh).
2026-09-03T01:20:21.745Z · Fable[A1 10:45 Фабл] P01 (EXPENSIVE_ADMISSION_ENABLED=1) включён на проде l94 после 30-мин окна: precheck paid=200/P11=1; после рестарта nc-a1 + ag-sip-native: paid=200 posture=enforce_ready mode=enforce; SIP readiness 200, stasis=1, ep7016=200. Бэкап .env.local.bak-p01-* в run-каталоге; откат = вернуть бэкап + рестарт nc-a1 + ag-sip-native.
2026-09-06T20:57:00.903Z · coordinator[06.09 20:56Z координатор] WEB-477 ПРИНЯТ (ACCWEB477IDEMPOTENCY, A2, GO): ec8e146b на 83683d48 — клиентская генерация x-idempotency-key (UUID на попытку, повтор той же попытки — тот же ключ) на всех платных POST; при PAID_OP_RECORD_ENABLED=1 повтор с тем же ключом → тот же результат без двойного списания (RED/GREEN), при =0 — byte-for-byte как база; SSE не сломан. → l113j. Включение PAID_OP_RECORD_ENABLED=1 — отдельным шагом после посадки с проверкой /api/chat-global через edge.
2026-09-06T23:52:21.447Z · astra-securityblock22-relay9-adjudication-web-477 — 2026-09-06T23:52:21.446667+00:00 / IST=2026-09-07T05:22:21.446674+05:30
BLOCK22 RELAY-9: вердикты по доставленному пакету. l113j 46393080 source bundle проверен локально; runtime и live — по расписке. Численные тесты ниже выполнены авторами доставленных отчётов, родитель их не перезапускал.
L113JPREP2-REPORT.md — новый отчёт.
Вердикт: GO for l113j stage-1 assembly at 46393080; deployed receipt confirms l113j, but stage 2 was only a probe and did not land.
Commit/base: 46393080dd885f6ff25967f18639075564e12737 / 18f130938b8d2a8f4376811651ad21beb4e28838
Доказательства: check-migration-compatibility rc 0, migrationCount 189; check-api-perimeter rc 0; realtime 61/61; active/passive lease 11/11; landing/proxy/final HTTP 56/56; composer DOM matrix pass; WEB477 23/23 plus negatives; Notion 11/11; SIP/genconfig/barge-in suites pass; activation checks pass; git fsck rc 0; bundle verify rc 0
Anchors: monet-w34-activation-manifest.json; monet-w35-activation-attestation.json; src/lib/billing/generatedActivationManifest.ts; public/_landing.html; src/proxy.ts; infra/sip/*
KNOWN ISSUES / ТРАБЛШУТИНГ: Stage 2 NOT on j: roles5b, docengine R2, controller R2, invite, podcast, beta, citation, overdraft, voice retry. Landing WEB-545 rewrite caused / 500 after deployment and was mitigated by PUBLIC_LANDING_ENABLED=false; fix is wave 2495. Keep WEB-545 landing label separate and do not overwrite SEC545 Lighthouse. Canonical SEC-028/WEB-545 remains Lighthouse; landing label collision is recorded only as provenance.
Действие: Use l113j only for deployed stage-1 contents. Route deferred stage-2 work through l113k/accepted waves; keep landing rewrite under 2495.
Report: /Users/poolpooly/audit/evidence/block22-relay9/intake-originals/a2/L113JPREP2-REPORT.md
SHA256: 8ff5adf24deb0d47091e4e1a5faa38676c582a7337777f6ec18e1babccdce7b3
КАРТА ДОКУМЕНТОВ: /Users/poolpooly/audit/reports/block22-relay9-update.md; /Users/poolpooly/audit/evidence/block22/ledger46.json; /Users/poolpooly/audit/evidence/block22-relay9/adjudications.json. Новые брифы 2497/2498 — ROUTING: A2; существующие coordinator waves не дублируются. Правила обогащения: WEB-449.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-477","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-03T06:12:22.358Z