WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-316 · Дефект · — · web

P1: Голос с доком (browser realtime) не подключается — «Connecting…» навсегда; корреляция с egress-блоками сторожа в ту же минуту

Закрыт P1 · важно ведёт: backup-opus
Суть
# Голосовое общение с доком мертво: «Connecting…» не завершается (repro owner 23.08 ~01:14 IST, прод 26116cff)

## Репро (owner)
1. Тетрадь Big-2 → панель «Асистент» (агент Універсал) → нижний микрофон.
2. macOS спрашивает доступ к микрофону (значит getUserMedia дошёл) → «Connecting…» висит вечно, «Start speaking to see the transcript…» пустой.
3. Иконка микрофона НЕ мигает красным во время беседы (раньше мигала); повторное нажатие НЕ отключает микрофон на маке (mac-индикатор остаётся).
Скрин: Снимок 2026-08-23 в 01.14.43 (owner, Интел).

## Смокинг-ган из прод-журнала (время совпадает с тестом)
journalctl -u note-clone-3010, 23.08 01:13:15 и 01:14:21 IST — шесть блокировок подряд:
```
[billing][unauthorized_vendor_egress_blocked] url: 'https://openrouter.ai/api/v1/models' | /api/v1/key | /api/v1/models/user
UnauthorizedVendorEgressError: this call did not go through the spend gateway… Route it through meterVendorCall/meterOpenAIClient/MeteredAIProvider
callSite: .next/server/chunks/41606.js:617 → app/api/models/openrouter/route.js
```
плюс «Sync error: ECONNRESET» сразу после каждой пачки.

## Гипотезы корня (по силе)
1. Realtime-препflight панели тянет каталог/ключ OpenRouter (/api/models/openrouter) → сторож расходов режет (это НЕплатные каталожные вызовы, но идут мимо gateway) → цепочка подключения не завершается → вечный Connecting. Прецедент точно этого класса: WEB-039 (ограда рубила Telegram-бота).
2. Сам realtime-канал (ephemeral token / WebRTC SDP к OpenAI) тоже не проведён через spend gateway → блок.
3. UI-стейты (красная пульсация, toggle off, освобождение mac-микрофона) — регресс контролов Browser Realtime: см. паркованную серию WEB-50…56 (Stop не отпускает local mic track — WEB-53 буквально об этом).

## Эволюция вопроса (обязательное чтение для нулевого агента)
- WEB-010 (review): WebRTC-микрофон молчит — прямой предок симптома.
- WEB-14 (done): переезд чат-микрофона со старого Web Speech на realtime.
- WEB-47…56 (parked): вся серия Browser Realtime preflight/Stop-контролов, включая WEB-53 (Stop must not wait for local mic track stop) и WEB-54 (preflight must not block controls).
- WEB-292 (done): Google realtime не распознавал входящее аудио.
- WEB-039 (review): ограда egress рубила Telegram — тот же механизм блока.
- WEB-060 (review): chat-global 403 model_not_allowed — модельные allowlist-стены той же природы.
- WEB-308 (done): включение enforce spend-gateway (источник ограды) — посадки 30/31, эпик приёмки WEB-314.
- Память контура: «Починка одного слоя ≠ работающая фича» — чат уже дважды умирал об вторую стену (capability_bad_signature у сторожа).

## План (bounded волна)
1. Network-трейс из браузера: какой запрос висит (models catalog? session token? SDP)? Точный URL+status.
2. Прод-журнал в момент повтора: связать blocked-URL с шагом подключения.
3. Фикс: каталожные/realtime вызовы провести через spend gateway (meterVendorCall) ЛИБО в allowlist неплатных каталогов — решение по спенд-политике зафиксировать в тикете.
4. UI: красное состояние записи, toggle-off с освобождением mac-микрофона (getTracks().stop()) — с ручной проверкой на маке.
5. Негативный тест: заблокированный vendor-вызов в realtime-цепочке обязан давать видимую ошибку в UI, а не вечный Connecting.

## WEB-316 server egress fix (2026-08-23)
WEB316_SERVER_FIX_READY — branch agent/web316-realtime-egress-20260823, commit 67d1e1f9. OpenRouter catalog now uses registry-bound zero-cost accounting; OpenAI/Google bootstrap and /me,/me2 token mints use paid gateway. Structured blocked response renders in the panel. Negative tests pass; no deploy. Report: /home/ubuntu/waves/WEB316-SERVER-REPORT.md.

## 2026-08-23 10:51 UTC — ЗАДЕПЛОЕНО в прод (посадка 32, v4-4482925)
Фикс в составе rel-4482925 (source 44829258a9398ebef6aedfeb9a959b7be4fbd4d3), аттестованный cutover, paidReady=true. Байт-маркеры фикса проверены в standalone до посадки (см. L32V2-REBUILD-REPORT.md на A1). Дальше: живая приёмка глазами в браузере против serving v4-4482925 (правило visual-acceptance) — после неё закрытие.


## 2026-08-23 11:07 UTC — ⚠️ ЖИВОЕ РЕПРО OWNER на v4-4482925: голос молчит, «Connecting…» висит
Owner (скрин в переписке): мак спрашивает доступ к микрофону, owner даёт, но индикатор не мигает красным (раньше мигал), ассистент молчит, с нотой и без; повторный клик НЕ выключает микрофон; бейдж «Connecting…» залип. ФАКТ из журнала Pi за окно теста (09:51-09:56Z): НОЛЬ записей realtime/bootstrap/websocket — клиент виснет ДО сервера или маршрут не логирует (правило: ноль ошибок ≠ фича жива). Значит вторая стена в цепочке (браузер-клиент → gateway/egress), фикс 316 в посадке 32 закрыл только свой слой. Статус назад в in_progress. Идёт: M4 l32accept (поверхность realtime против v4-4482925); mini-фикс незалипающего выключателя — в брифе web315r (п.5). Дальше по отчётам — точечный диагноз цепочки.


## 2026-08-23 11:35 UTC — приёмка l32accept: GET /api/realtime = 404 на serving
M4: безопасная проба поверхности — авторизованный GET /api/realtime вернул 404 Not Found (сессию/бутстрап сознательно НЕ стартовали, микрофон не жали — owner-QA скоуп). При этом байт-маркер «Realtime bootstrap gateway rejected» в артефакте есть (.next/server/app/api/realtime/route.js). Гипотезы: роут только POST (GET 404 легитимен) ИЛИ он не подключён в serving. Вместе с owner-репро «Connecting…» вечно → следующий шаг: точечный диагноз бутстрапа (метод/статус POST c невалидным телом, network-трейс из клиента).


## 2026-08-23 11:45 UTC — мини-фикс выключателя микрофона ВОШЁЛ в web315r
Найден in-flight race: второй клик приходил пока connect() гидратировал runtime profile (isConnected ещё false) — клик игнорился. Теперь ChatInterface.toggleRealtime: второй клик отменяет pending connect, останавливает fallback/observer, зовёт disconnect (12 строк, в коммите f3898a74). Это чинит «повторно нажать — не выключает». ГЛАВНАЯ стена («Connecting…» вечно, ассистент молчит) — ОТДЕЛЬНАЯ, ещё в диагнозе (GET /api/realtime 404 на serving, нет network-трейса бутстрапа).


## 2026-08-23 14:57 UTC — ЗАДЕПЛОЕНО: посадка 33 (v4-00c402c) села с 1-й попытки
rel-00c402c (SHA 58af4a94…), аттестованный cutover c НОВЫМ пин-гейтом (все 7 env-пинов сверены с аттестацией ДО флипа — негативный гейт из урока посадки 32). paidReady=true, канон проштампован. Живая приёмка фикса — следующий шаг.
Мини-фикс выключателя микрофона в этой посадке; главная стена Connecting — отдельный диагноз.

## 2026-08-24 00:30 UTC — статус на вопрос owner («не починили или не посадили?»): НЕ ПОЧИНИЛИ, в диагнозе
Посажено из цепочки: только мик-выключатель (гонка второго клика, f3898a74, посадка 33). ГЛАВНАЯ стена (Connecting навсегда, молчание) — не диагностирована до конца: клиент виснет ДО сервера (0 realtime-записей в журнале за тест owner; GET /api/realtime 404 — но POST-роут в артефакте есть). ПЛАН: после shadowrun (M4, ~20 мин) — диагност-волна: браузерный network-трейс клика микрофона (bootstrap-этап бесплатный), проверка куда клиент шлёт bootstrap/WS и что получает; помнить про возможную ВТОРУЮ стену (egress-сторож, capability_bad_signature — прецедент 20.08). Полный голосовой раунд = owner-QA после фикса.


## 2026-08-24 00:40 UTC — диагност-волна ЗАПУЩЕНА РАНЬШЕ плана (A1 web316diag, Luna xhigh, read-only): статический трейс клика до первого network-вызова, ранжированные гипотезы «где молчит», сервер-ожидания bootstrap + egress-capability след (вторая стена). По отчёту — минимальный браузерный чек-лист на M4.


## 2026-08-24 01:30 UTC — 🎯 ГЛАВНАЯ СТЕНА НАЙДЕНА: /api/realtime задушен excluded-зоной «telephony» (WEB-074 deny-default)
Диагност-волна (WEB316DIAG-REPORT, полная карта цепочки файл:строка) + живые пробы координатора:
1. СТЕНА №1 (корень 404): route.js ЕСТЬ в релизе, манифест ЕСТЬ, но GET /api/realtime = 404 → режет middleware: '/api/realtime' входит в зону id='telephony' (excludedZones.ts:21), а она в DEFAULT_EXCLUDED_ZONE_IDS. Включение deny (WEB-074, принятый нами же!) задушило браузерный голос за компанию с SIP. Хронология сходится: у owner мак СПРАШИВАЛ микрофон (цепочка доходила до getUserMedia), затем bootstrap бился о 404 и…
2. СТЕНА №2: …ошибка ТОНУЛА — toggleRealtime catch не логирует и не кладёт non-fatal в state (правило 10), phase застревает в connecting навсегда.
3. СТЕНА №3 (потенциальная): hydrateRuntimeProfile fetch без timeout/AbortSignal — вечное ожидание при зависшем /api/runtime-profile.
ФИКС-ВОЛНА web316fix (Luna xhigh): новая зона browser-realtime ВНЕ default-deny (телефония остаётся 404 — негативами), таймаут гидрации, громкий catch с [WEB316] и видимой ошибкой. Посадка экспрессом 35.1 сразу после 35. Затем owner-QA полного голосового раунда.


## 2026-08-24 02:00 UTC — ФИКС ГОТОВ (WEB316_FIX_READY) и ВКЛЮЧЁН 7-м грузом в посадку 35
web316fix: зона browser-realtime (5 префиксов /api/realtime*), /api/embed/realtime осознанно ОСТАВЛЕН в telephony; негативы: телефония/meeting-rooms 404 как раньше, /api/realtime → 401 unauth (дошёл до auth!), PROD_EXCLUDED_ZONES=all → 404 (opt-in работает), generated edge deny --check зелёный. Гидрация: AbortSignal.timeout(10s) + hard-deadline race (даже мок, игнорирующий abort, не подвесит connect), 401/403-семантика сохранена, прочее — в realtimeState.error. Catch: громкий [WEB316]-лог + видимая ошибка + phase не застревает. Патч спасён (204a168c…). l35fix перезапущен с этим грузом — голос поедет посадкой 35, без отдельной 35.1.


## 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-24 03:35 UTC — стена №2 доказана живьём + найден и снят ПОСЛЕДНИЙ засов (№4)
Owner-скрин после посадки 35: видимый «Voice error» бейдж с человеческим текстом — тихий catch закрыт ЖИВЬЁМ. Новая (финальная) стена: «Live voice is not available in this release» = releasePosture.ts: при armed containment default deny_release1, хотя манифест содержит realtime.browser.{openai,google}.session с releasePosture=ALLOW и bounded max_realtime_session_sec. РЕШЕНИЕ: Environment=REALTIME_RELEASE_POSTURE=legacy_premint в dropin (явная поза выигрывает резолюцию; консистентно с ПОДПИСАННЫМ манифестом), рестарт, paidReady=true, realtime 401-путь жив. Owner позван на повторную пробу. Итог цепочки стен: №1 зона (посажено 35) → №2 тихий catch (посажено 35, доказано скрином) → №3 гидрация-таймаут (посажено 35) → №4 release posture (env-флип). Ждём полный голосовой раунд owner-QA.


## 2026-08-23 16:21 UTC — web316c СДАН: observed posture (Variant B) реализован (волна A1, Luna xhigh)
Пятая стена голоса снята. Реализация REALTIME-DECISION-R4.2 §Option B ТОЛЬКО для browser_realtime_mint (embed/bridge/telephony остаются fail-closed): VoiceSession-леджер на mint до provider dispatch (observedSessionLedger.ts), ephemeral token → SHA-256 binding + unique index (миграция 20260823160000_web316c_observed_realtime_ledger), receipts session-start/turn/end, bounded duration из MAX_REALTIME_SESSION_SEC (обязателен>0 в enforce observed), reap просроченных (/api/cron/realtime-reap + sweep из spend-reconcile), fail-closed boot через именованный контракт REALTIME_SESSION_LEDGER. Негативы: без auth 401; дубль токена отклонён; late turn после expiry 409; legacy_premint по-прежнему отвергается; без позы deny_release1. Тесты 103/103 posture/containment/ledger/lifecycle, scoped tsc rc=0, prisma validate pass. Коммит 0056bc3f6, ветка web316c. Env посадки 37: REALTIME_RELEASE_POSTURE=observed + MAX_REALTIME_SESSION_SEC>0. Груз l37build (собирается СЕЙЧАС).

## 2026-08-23 BOARDSWEEP: STALE-NOT-DEPLOYED
- Evidence: Canonical contains realtime session-start/egress guard code and tests, but buildFixed is empty; board trail requires a later live fix/voice acceptance. Unauth GET /api/realtime=401; no paid realtime session started.
- buildFixed: <empty>; canonical: v4-081ff4fb5 / 081ff4fb5f6110f8001daec7ed910ccef2e395dc.
- Status preserved by sweep: review.
Доказательства
[24.08 ~00:30 — статус к компакту] Голос в работе: сборка 40 (l41voice на A1) интегрирует WEB-316-voicefix.patch (method-aware guard: SpendGuardInput.method + guardRealtimeMint передаёт realtime.session, тест voicefixSpendGuard.test.ts) + WEB-344 (transcriptions через meterVendorCall) + патчи душ. КРИТЕРИЙ приёмки перед посадкой 40: grep method в SpendGuardInput артефакта + зелёный voicefixSpendGuard-тест. ПОСЛЕ посадки 40 — env-активация §6 (observed/1800с/per_second:0.0011/$2.50/$8-день), затем позвать owner на живой тест микрофона. Эволюция: prisma-500 (37.1) → env-активация 23:30 → owner-тест выявил model_not_allowed → корень: guard не method-aware в проде (SpendGuardInput без method) → откат env к deny_release1 → фикс в посадку 40. Транскрипт owner-теста: ~/Downloads/chat-json-2026-08-23T22-29-42-250Z.json. Рецепт: /home/ubuntu/waves/VOICEENABLE-REPORT.md. Родня: WEB-344, WEB-010/WEB-317 (retest после голоса), WEB-320.

[24.08 00:50] ПОСАДКА 40, попытка 1 (l41): сборка A1 готова (отчёт /home/ubuntu/waves/L41VOICE-REPORT.md; WEB-316 APPLIED 6/6, WEB-344 guard 2/2, WEB-273 78/78, WEB-310 3/3+миграция dedupeKey, WEB-045 5/5). Коммит 45a2707, артефакт ff322ac9, аттестация подписана, миграция 20260823190000_web310 применена (prisma 5.22 из npx-кэша Pi, схему пришлось дописать — в артефакте нет schema.prisma). ФЛИП: paidReady=False → АВТООТКАТ ок (простой ~18с, прод на l39 def2df3f7 зелёный). КОРЕНЬ: prebuild волны шёл БЕЗ env SPEND_DEPLOYMENT_MANIFEST/MODEL_ALLOWLIST → генератор manifest потерял операторские SKU (tts/chat: gpt-4o-mini-tts, gpt-5.5-nano, claude-haiku-4.5, gemini-2.5-flash*) — runtime кросс-чек MANIFEST упал по 6 строкам. УРОК: generatedActivationManifest env-зависим — сборочный env обязан содержать боевые SPEND_DEPLOYMENT_MANIFEST+MODEL_ALLOWLIST. ПЕРЕСБОРКА l41b (tmux l41b, лог /home/ubuntu/waves/l41b-rebuild.log): генерация с боевым env + строка openai|realtime.session|gpt-realtime-mini|realtime (=8 упоминаний, кросс-чек активации голоса пройдёт), ROWCHECK 6/6 SKU, migrationSet 123 d9d1bd5c сохранён. Маркер: ARTIFACT_READY_L41B.


[24.08 01:10] ПОСАДКА 40 СЕЛА со 2-й попытки (l41b): пересборка с боевым env вернула SKU (манифест байт-в-байт как l39, aed050b7) + migration set 123. Коммит c77e91d1d, артефакт 9cd3bdbe, пин-гейт 7/7, L41B_LANDED_OK, paidReady=True. ГОЛОС АКТИВИРОВАН: REALTIME_RELEASE_POSTURE=observed, MAX_REALTIME_SESSION_SEC=1800, строка openai|realtime.session|gpt-realtime-mini|realtime в runtime-манифесте, NON_TOKEN_PRICE_OVERRIDES per_second:0.0011, reserved $2.50, daily realtime_bootstrap $8.00 (капы owner-approved 23.08). paidReady=True после рестарта. Бэкап env: spend-runtime-c77e91d1d.env.bak-preVoice. Owner позван на живой тест микрофона (tg id=13128). Осталось: живой тест owner → закрытие; RETEST WEB-010/WEB-317.


[24.08 16:20] Owner-тест микрофона #2: стена №3 — reservation_too_large: сессия резервирует $2.50 (owner-кап), а MAX_SINGLE_REQUEST_RESERVATION_USD=0.10 (spendReservation.ts:672-679). ФИКС: лимит поднят до 2.50 в spend-runtime env (бэкап .bak-voicecap), рестарт, paidReady=True. Owner позван на ретест (13154). Хронология стен: 1) model_not_allowed (guard не method-aware → посадка 40), 2) [между] env-рецепт, 3) пер-реквест резерв $0.10 → поднят.


[24.08 17:00] Стена №4 (owner-ретест #3): operation_budget_exhausted — MAX_OPERATION_RESERVED_USD=0.20 < резерв $2.50 (operationBudget.ts:81). Поднят до 2.50 (бэкап .bak-opbudget), рестарт, paidReady=True, owner позван (13162). Оба операционных потолка (пер-реквест + операция) теперь = owner-кап $2.50; дневной realtime_bootstrap $8 не тронут. Телеметрия движка: вердикт по 168 записям — 100% divergence unrepresented-dom-mutation → WEB-347 P1 (этап движка НЕ принят).


[24.08 20:10] ГОЛОС РАБОТАЕТ — фича подтверждена end-to-end. После снятия кап-стен (№3-6) сессия живёт целиком: транскрипт owner 4 хода (chat-json-2026-08-24T13-24-20, вкл. «восстание Спартака» — корректный ответ), сервер минтит realtime с output_modalities=['audio','text'], аудио-трек уходит по WebRTC. МОЛЧАНИЕ — на стороне Мака owner: YouTube тоже без звука, наушников нет, громкость не выключена → системный аудио-выход пропал/перехвачен (не наш баг). Owner дан фикс (Output device в меню-баре/Звук, закрыть запись экрана; 13170). ОСТАЁТСЯ экономика: WEB-348 (settle-at-max, $10 за короткий тест). WEB-316 как «голос не подключается» — ЗАКРЫВАЕМ (подключается и работает); хвост method-aware/env — в проде. Кандидат на closed после того как owner услышит звук.


[24.08 21:10] ✅✅ ГОЛОС ЗАГОВОРИЛ (owner подтвердил скринами): звук появился после killall coreaudiod — был завис аудио-демон Мака owner, НЕ наш баг. Полный путь голоса подтверждён живьём: слушает→отвечает→ГОВОРИТ. Хвосты: WEB-348 (settle-at-max, экономика), WEB-349 (косметика: мигающий cancel-warning при перебивании). WEB-316 функционально ЗАКРЫТ — голос с доком работает. Кандидат в closed.
Замечен в сборке
26116cff
Связи
Лента
2026-08-25T07:11:44.164Z · fable-ops
ПРИЁМКА (закрытие). Owner подтвердил живьём (24.08 21:10, скрины): полный путь голоса слушает→отвечает→ГОВОРИТ. Молчание было завис coreaudiod на Маке owner (killall починил), НЕ наш баг. Прод v4 посадка 40+. Хвосты вынесены в WEB-348 (экономика settle-at-max) и WEB-349 (косметика cancel-warning). Функционально закрыто.
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-316","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-25T07:11:44.211Z