WEB-15 · Дефект · Голос · web
Meeting Room browser must visibly indicate assistant speaking/audio state
Закрыт
P1 · важно
ведёт: —
Суть
ПАРКОВКА 05.08 (решение owner-а): зона голоса/телефонии/Realtime у изолированного Codex-инженера — контур не трогает. Связка 17/47/50-54 откачена волной AY (revert 5a0466a79). Карантин: /tmp/WEB-MR.patch на интеле ждёт его ревизии.
## 2026-08-23 11:20 UTC — разбор парковки (по запросу owner): почему здесь и что известно
Зона: телефония/Browser Realtime — исторически ИЗОЛИРОВАННАЯ зона внешнего codex-инженера (WEB-15/17/45-56), контурным воркерам сюда нельзя; тикеты создавались как заголовки-закладки той эпохи (апрель, agent_tasks_sip/voice_realtime), тела не расписывались. Припарковано чтобы контур их не хватал.
⚠️ АКТУАЛИЗАЦИЯ 23.08: голосовая цепочка снова горит (WEB-316: owner — «Connecting…» залип, мик не выключается; WEB-318 SIP). Уточнение owner: 8000/8010 = codex/opus выключены; звонки в тетрадь = 7015/7016. После диагноза WEB-316 часть этих закладок либо поглотится фиксом, либо будет закрыта как устаревшая. Решение по расконсервации зоны — за owner (внешний инженер).
[01.09 15:20Z] ПРИНЯТО живой пробой OWNER-А на проде l83 (после включения MEETING_ROOMS_MROOM1_MULTICHANNEL_ENABLED): скрины owner — transport connected/openai, State=Listening «assistant is waiting for the next user turn», ASSISTANT=listening (НЕ unknown), индикатор «слушает» на плитке ассистента, SPEECH LATENCY=818ms (ассистент реально говорил). Сервер: room_audio_session_binding → диалог → normal_close 15:14:24Z. Критерий BOARDTRIAGE выполнен: реальное speech/audio state вместо unknown, отказы честные. GO → done. Остаточный косметический дефект teardown вынесен в WEB-427.
Доказательства
## 2026-08-28 ~12:55Z — ПАРКОВКА СНЯТА ВЛАДЕЛЬЦЕМ, тикет взят в работу (координатор Фабл)
Owner 28.08: «Тикеты с паркинга да можно брать (я писал что инженер по телефонии у нас поднакрылся)». Решение от 05.08 об изолированной зоне внешнего codex-инженера отменено — линию ведёт контур.
Основание приоритета: свод MROOMSPEC по 24 тикетам комнаты показал, что доказан только ТРАНСПОРТ (три плеча сходятся, звук в обе стороны), а требования про ассистента как участника — `НЕ ДОКАЗАНО` по всем плечам, по Signal `НЕ РЕАЛИЗОВАНО`.
Волна заряжена; запретная строка про зону телефонии из брифа убрана явно, чтобы волна не останавливалась на границе.
Волна `web15`: состояния ассистента (говорит/слушает/думает/молчит), доставка событием, проверка отдельно по трём плечам, задержка в числах, поведение признака при обрыве.
## 2026-08-28 ~12:00Z — СЛЕД ДЛЯ ПОДХВАТА (координатор Фабл, перед компактом)
**Линия:** комната, ассистент как участник. Парковка снята владельцем 28.08 (внешний инженер по телефонии выбыл).
**Эволюция:** `web15` — состояния ассистента (говорит/слушает/думает/молчит), доставка событием. Отчёт: `A1:/home/ubuntu/waves/WEB15-REPORT.md`. → `ACC15` = **NO-GO**, отчёт `A1:/home/ubuntu/waves/ACC15-REPORT.md`. → **идёт `web15b`**.
**Требования обязательные:** таблица по ТРЁМ плечам отдельно (браузер, SIP, сигнал) — общий вывод не принимается; задержка от начала речи до появления признака числом; признак гаснет при ошибке, обрыве и уходе участника; при неизвестном состоянии показывать «неизвестно», а не «молчит».
**Контекст:** свод требований `A1:/home/ubuntu/waves/MROOMSPEC-REPORT.md` — по этому пункту `НЕ ДОКАЗАНО` по всем плечам.
[28.08 15:20Z Фабл] ACC15B = NO-GO, 5 причин: SIP-маркер определён, но прод-путь его не вызывает; Signal mockOnly/not_started; поздний event старой инстанции воскрешает speaking (cancelEpoch не сбрасывается); состояние только в памяти клиента (reload теряет); ошибки SIP delivery/Whisper проглатываются. Заряжен web15c (очередь 262): подключить SIP прод-путь, честное состояние «плечо не подключено» для Signal (полный Signal — вне волны, отдельное решение владельца), epoch-сброс, восстановление после reload, видимые ошибки. Отчёты: ACC15B-REPORT.md, будет WEB15C-REPORT.md.
[28.08 19:50Z Фабл] ACC15C = NO-GO: SIP-wiring из прод-пути ПОДТВЕРЖДЁН, ограничение по Signal принято приёмщиком; остатки epoch — поздний старый OpenAI-audio после старого response.done проходит, равный timestamp пропускает старую инстанцию. Заряжен web15d (эпоха+монотонный счётчик вместо timestamp). Отчёты: ACC15C-REPORT.md.
[28.08 17:45Z, влито] ACC15D NO-GO: Б1 эпоха процесс-локальна при durable-ключе; Б2 speaking после неудачной записи; Б3 три старых тихих catch → web15e (опус). Отчёты: ACC15D-REPORT.md.
[28.08 19:45Z Фабл] ЭВОЛЮЦИЯ ИНДИКАЦИИ: web15 → ACC15 NO-GO → web15b (4 состояния, 3 плеча) → ACC15B NO-GO (5 причин: SIP-маркер не в прод-пути, Signal not_started, воскрешение speaking, память клиента, глотание ошибок) → web15c (SIP wiring в sessionStart.ts:1334-1393, epoch) → ACC15C NO-GO (поздний audio, равный timestamp) → web15d (эпоха+монотонный счётчик) → ACC15D NO-GO (Б1: эпоха процесс-локальна при durable-ключе; Б2: speaking после отказа записи; Б3: 3 старых catch) → web15e (durable generation) → ACC15E NO-GO (B1-new: SIP-путь теряет durable generation) → web15f СДАН → TODO приёмка acc15f. Отчёты: A1:WEB15*-REPORT.md, ACC15*-REPORT.md. Signal-плечо: честное not_connected (решение владельца — полный Signal отдельно).
[28.08 ~20:05Z ACC15G: NO-GO — гонка без атомарности + матрица epoch проходит наполовину]
Отчёт: /home/ubuntu/waves/ACC15G-REPORT.md, собственная проба приёмщика /home/ubuntu/waves/acc15g-probe.ts.
ЗЕЛЁНОЕ: старый epoch 4 не вытесняет текущий epoch 5; другая комната изолирована; последовательный перезапуск процесса durable generation переживает (first: 1 → restarted: 2).
БЛОКЕР 1 — ГОНКА: два одновременных контекста в одной комнате на одном ключе, с барьером заставляющим обоих прочитать пустой ledger до записи, получили ОДИН И ТОТ ЖЕ generation 1. В ledger две записи, владелец race-B, но race-A уже считал свой claim успешным. Корень: claimAssistantSpeechGeneration делает read → write → reread, что НЕ атомарно. Приёмщик прямо указал: нужна серверная атомарная выдача или уникальная защита, а НЕ ещё один локальный retry. Разделение leg-a/leg-b (независимые floor) гонку на одном ключе не исправляет.
БЛОКЕР 2 — матрица повреждённых epoch: empty=false, null=false, string=false, negative=false, **huge=true**, invisible=false, **duplicate-valid=true**. Значения huge и duplicate-valid принимаются, должны отвергаться.
Круг запущен: WEB15G (очередь 303, база l65) — атомарная выдача через уникальное ограничение или атомарную операцию (локальный ретрай не примут), общий строгий разбор epoch с верхней границей и отказом на недопустимом повторе, прогон пробы приёмщика как есть с таблицей было/стало, свой негативный тест. Миграции только аддитивные.
---
## 2026-08-28 21:30Z — ACC15H: GO. Атомарное устройство принято → review
- Коммит 12704970 (wt-web15g): выдача generation одной операцией INSERT ... ON CONFLICT (roomId,key) DO UPDATE ... RETURNING; уникальный индекс = точка сериализации; read→write→reread и retry убраны.
- Приёмка /home/ubuntu/waves/ACC15H-REPORT.md: гонка → один победитель/одна строка; чужой epoch отвергнут; рестарт переживается; матрица мусора 8 ячеек fail-closed; адверсариальная проверка заявления «duplicate-valid — дефект harness» подтвердила автора (дублированный JSON отвергается на настоящей границе).
- Миграция 20260828100000_web15g_atomic_assistant_generation аддитивна.
- НЕ в l66 (слит позже сборки) — кандидат следующей посадки.
---
## 2026-08-29 04:15Z — доливается в l67 отдельной волной
Принятый коммит 12704970 (ACC15H GO, атомарная выдача generation + миграция 20260828100000_web15g_atomic_assistant_generation) не попал в первый заход слияния — волна остановилась раньше на грузе 415. Запущена L67MERGE2: доливает 12704970 и c1276742 поверх текущей l67. Миграция аддитивная, применяется по конвейеру при посадке.
[29.08 ЖИВАЯ ПРОВЕРКА КОМНАТЫ НА ПРОДЕ l68]
КОМНАТА ВЫКЛЮЧЕНА ФЛАГОМ: серверный лог сыплет каждые ~2 сек MeetingRoomServiceError: «Meeting Rooms generic API is disabled until an explicit local/test gate is enabled», code: meeting_rooms_api_disabled, из .next/server/app/api/meeting-rooms/v1/rooms/[roomId]/route.js. Голосовое плечо не поднимается, состояния «ассистент говорит» достичь нельзя. Флаг не включал — это правка серверных настроек.
ПОЛОЖИТЕЛЬНОЕ ПО САМОМУ ТИКЕТУ: индикация состояния речи ЕСТЬ и НЕ ВРЁТ — интерфейс пишет «State unknown — No trustworthy assistant speech event is available», ASSISTANT: unknown, SPEECH LATENCY not measured, вместо ложного «говорит». Это ровно то поведение, которого требовал тикет.
ПОБОЧНЫЕ НАХОДКИ: путь /meeting-rooms отдаёт 404 (в сборке нет page.js на этом уровне), рабочий путь /meeting-rooms/room; циклический тост «Room state: room_state_http_500»; после «Start Browser Realtime» — красная строка «assistant.speech_state participant is not the room assistant»; данные комнаты выглядят демонстрационными (NCS · MR-001, «вчера, 47 мин», «14 решений»).
ВЫВОД: тикет нельзя закрыть живой пробой, пока комната выключена флагом на проде. Нужно решение: включаем ли флаг на малинке (это дев-контур, пользователей нет) — тогда проверка возможна.
[29.08 УТОЧНЕНИЕ: это НЕ забытый флаг, а сознательная защита]
src/app/api/meeting-rooms/v1/_shared.ts:52-80, assertMeetingRoomsGenericApiRuntimeBoundary требует ОДНОВРЕМЕННО: MEETING_ROOMS_ENABLE_GENERAL_API==='1'; непустой DATABASE_URL; совпадение с MEETING_ROOMS_TEST_DATABASE_URL если он задан; assertLocalMeetingRoomsDatabaseUrl(databaseUrl) — то есть ЛОКАЛЬНУЮ базу; и ОТСУТСТВИЕ живых ключей — запрещённые префиксы включают TWILIO_ и WHATSAPP_(ACCESS|CALL|PHONE|WEBHOOK)_.
Значит generic API комнат намеренно сделан локально-тестовым и не может работать там, где есть живые ключи телефонии. Просто «включить флаг» на проде нельзя — защита осмысленная, снимать её я не стал.
ОТКРЫТЫЙ ВОПРОС, на который у нас нет ответа: каким путём мультирум должен работать в ПРОДОВОЙ среде? Заряжена волна MROOMPROD (очередь 457): карта всех путей клиента комнаты, проходит ли каждый через этот гейт, существует ли продовый путь вообще или комната задумана локально-тестовой; заодно почему /meeting-rooms отдаёт 404, откуда room_state_http_500 и что означает «participant is not the room assistant».
[29.08 ДИАГНОЗ MROOMPROD (NOCOMMIT, аналитика): найден точный корень — ошибка маршрутизации клиента]
Не «забытый флаг» и не «задумано только локально». Production-путь ЕСТЬ, но клиент комнаты ходит за состоянием не туда:
- страница создаёт комнату через POST /api/meeting-rooms/v1/owner/rooms — owner-scoped, production-safe;
- MeetingRoomV2Client читает и опрашивает состояние через GET /api/meeting-rooms/v1/rooms/:roomId?include=state — generic путь, намеренно закрытый для прода;
- у owner API СООТВЕТСТВУЮЩЕГО маршрута GET state НЕТ — prod-safe REST комнаты не достроен.
Соседние owner-роуты существуют и работают: owner/rooms/:roomId/end и .../summary (MeetingRoomV2Client.tsx:331-351), с auth + owner/moderator ACL.
Остаток: voice/multichannel код browser Realtime имеет production-oriented binding path, но зависит от отдельного runtime-флага и release posture; Signal в комнате не стартует вовсе и помечен в коде как mockOnly/liveGated/not_started.
ВЫВОД: снимать generic boundary НЕЛЬЗЯ — он не даёт тестовому API работать с реальной общей базой и живыми ключами телефонии. Нужно ДОСТРОИТЬ owner-путь. Заряжена волна MROOMOWNER (очередь 464): owner-маршрут состояния + перевод клиента + таблица всех запросов клиента + понятный отказ вместо room_state_http_500 + судьба 404 на /meeting-rooms.
[29.08 ПРИНЯТО — ACCMROOMOWNER_VERDICT=GO, коммит 5393a31b45e496d1f55f92f9521394983b0d8991]
Достроен owner-scoped маршрут состояния комнаты, клиент переведён на него. Приёмка провела ПОЛНЫЙ аудит обращений клиента комнаты (не по таблице автора, а сама) и подтвердила: критический generic endpoint клиентом НЕ вызывается. Три визуально похожих вызова под /v1/rooms/:roomId/... generic-ом не являются — у них свои границы и ACL: assistant/speech-state и .../generation (отдельный auth + assistant/leg/generation ACL), stage9/local-actions (Stage-9 request/runtime boundary); функция из generic _shared там не вызывается.
Generic boundary НЕ ослаблен — защита от работы тестового API с реальной базой и живыми ключами телефонии на месте.
Кандидат в линию после l69. ПОСЛЕ ПОСАДКИ обязательна живая проба: открыть комнату на проде и убедиться, что состояние читается (сейчас сыплет meeting_rooms_api_disabled каждые ~2 сек) и что индикация «ассистент говорит» получает настоящее состояние вместо ASSISTANT: unknown.
[29.08 ЖИВАЯ ПРОБА НА l70 — КОМНАТА РАБОТАЕТ]
POST /api/meeting-rooms/v1/owner/rooms -> 201, GET .../rooms/{id}/state -> 200 непрерывно (было 503). Состояние настоящее, не заглушка: room.id, title «Утренний синк», status active, notebookId, assistantEnabled:false, participants с ролью owner и consentStatus accepted, counts, recentEvents с room.created. Повторный вход по тому же roomId поднимает состояние с сервера, новую комнату не создаёт.
Помойка погашена: за 3 минуты при ~90 опросах (замерено 5 запросов /state за 10 сек) в логе meeting_rooms_api_disabled — 0, упоминаний meeting_room — 0, 503 — 0. Было каждые ~2 сек.
/meeting-rooms больше не 404: под логином рендерит комнату, без логина 307 на /onboarding (гейт авторизации).
⚠️ ОСТАЁТСЯ: индикация показывает «State unknown — No trustworthy assistant speech event is available» и ASSISTANT: unknown, хотя сервер уже знает assistantEnabled:false, assistant:null, assistantSpeechEvents:[]. Это не ложь, но и не то, что известно серверу — отдельная доработка.
Голосовое плечо не поднимается (ожидаемо, за отдельным флагом): «Signal-плечо: не подключено (signal_live_call_not_started)», «Browser Realtime — transport unavailable»; при Start Browser Realtime — «assistant.speech_state participant is not the room assistant». Отказ честный, ложного успеха нет.
[BOARDTRIAGE] Приёмщик: независимый browser-QA. GO только если на l70 живая комната показывает реальное assistant speech/audio state, а не unknown при доступном server state; transport/Signal отказ остаётся явным.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-15","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-01T15:15:48.029Z