WEB-425 · Задача · — · web
P1 Голосовая расшифровка в мессенджерах: 3 пути вызывают transcriber БЕЗ billing identity (fail-closed, фича мертва)
Закрыт
P2
ведёт: —
Доказательства
Найдено приёмкой денежного гейта (L66FIX2, сверка §7 ACCL65FIX1) на l66. Три пути ставят request scope, но не передают payer/billing identity в transcriber → падает до платного вызова, fail-closed (деньги целы, но голос из мессенджеров не расшифровывается): src/lib/messaging/signalVoiceHandler.ts:81-84 (нет resolveMessengerTranscriptionIdentity в transcriber(file,language)); src/lib/messaging/handlers/voiceHandler.ts:68-71 (transcriber(file) без billing identity); src/lib/messaging/voiceToNote.ts:41-49 (legacy transcribeAudioFile без identity). Класс тот же, что L65FIX1/L66FIX2 (identity не доезжает до вендорного вызова). Отчёт: /home/ubuntu/waves/L66FIX2-REPORT.md §7. Чинить единообразно: resolveMessengerTranscriptionIdentity на всех трёх путях. НЕ блокирует l67 (мертво и на l64).
---
## 2026-08-29 04:30Z — круг сдан, идёт приёмка
Коммит 8a5c5ead (база l66). Сделано: Signal-путь handleSignalVoice получает userId/sharedGuest от вебхука и передаёт billing identity в transcriber; общий handleVoiceMessage строит identity из message.userId/message.sharedGuest; legacy voiceToNote — по отчёту тоже закрыт.
Приёмка ACC425 (queue/365): проверить все три пути + САМОСТОЯТЕЛЬНО грепнуть ВСЕ вызовы transcriber/transcribeAudioFile на предмет четвёртого пути без identity; отдельный вопрос — sharedGuest: с кого списывается, если гость не привязан к плательщику (тихого списания на чужой счёт быть не должно).
---
## 2026-08-29 05:10Z — ACC425: NO-GO (узкий), круг 2 заряжен
- Принято: три пути (signalVoiceHandler:90, handlers/voiceHandler:71, voiceToNote:55) получают identity через resolveMessengerTranscriptionIdentity. Приёмка САМА проверила все messenger-вызовы transcriber/transcribeAudioFile (telegram webhook:3476, telegramVoiceRetryRunner:205, imessage bluebubbles:670, whatsappVoiceHandler:49) — четвёртого пути без identity в messenger-скоупе НЕТ.
- Не-messenger вызовы без billing identity перечислены и признаны вне скоупа: ingest/extractors/audio.ts:123 (фоновый ingest, userId только для слота), media/tiktok/helper/transcribe, media/transcribe-local, embed/transcribe, embed/voice-turn (свои политики/apiKey). Это НЕ дефекты этого тикета, но зафиксировано на будущее.
- БЛОКЕР: legacy WhatsApp ingress теряет messagingSharedScope.sharedGuest (canonical сохраняет его на route.ts:449-481) → расшифровка гостя списывается как за владельца. Круг WEB425B заряжен (queue/367): одна функция извлечения scope для обоих входов; неопределимый scope = fail-closed с маячком.
---
## 2026-08-29 06:55Z — ACC425B: GO. Голос из мессенджеров закрыт
Приёмка подтвердила коммит 6e782fd0: legacy WhatsApp сохраняет sharedGuest, единая функция извлечения scope для обоих входов, неопределимый scope = отказ (не «спишем владельцу»). Вместе с кругом 1 (8a5c5ead) линия закрыта: identity доезжает во всех messenger-путях расшифровки.
Кандидат l68. После посадки — живая проба голосом из мессенджера.
[29.08 ЗАКРЫТ ПО ЖИВЫМ ДАННЫМ ПРОДА l68 — учёт есть на всех путях, платных вызовов при проверке 0]
СХОДИМОСТЬ 1:1 ЗА 3 СУТОК: входящих голосовых (MessageLog type='voice') 109, из них failed 1 (до транскрайбера не дошло); строк расхода TokenUsage канала media.transcribe.openai.gpt4o-mini-transcribe — 108. То есть 108 расшифровок = 108 записей учёта, ни одной неучтённой. Сумма 317333 микро (~$0.317), все с реальным userId, pricingStatus=exact. SpendOperation: actionKey=media.transcribe, feature=media_transcribe, identityKind=USER, attemptCount=1.
РЕЖИМ ВООРУЖЁН (не ship-dark): SPEND_CONTAINMENT_MODE=enforce, REQUIRE_PAID_IDENTITY=1, ENABLE_REAL_SPEND_LEDGER=1, SPEND_RELEASE1_EXACT_METHODS=audio.transcriptions.create.
ЖИВОЙ ПРОГОН СЕГОДНЯ на l68 (09:34:05 и 10:09:02): квитанция [billing][settling_at_max] { channel: media_transcribe, method: audio.transcriptions.create, family: stt }, следом messaging.resolveSources -> chatWithSources. Полный путь голосовое -> расшифровка -> учёт -> ответ прошёл.
НЕГАТИВ ТОЖЕ РАБОТАЕТ: 28.08 19:38-19:42 (ещё l65) — 4 строки operation=billing_guard_request_identity с NULL costMicros и [transcribe] gpt-4o-mini-transcribe failed. Гейт отказывает, а не пропускает.
УСТРОЙСТВО: единственная точка выхода к провайдеру — getOpenAIClient в src/lib/media/transcribe.ts:104-113, она БЕЗУСЛОВНО оборачивает клиент в meterOpenAIClient; грепом по дереву прямых вызовов audio.transcriptions мимо неё нет. Плюс сетевой заслон SPEND_EGRESS_BOUNDARY_MODE=proxy и [billing][egress_direct_deny_self_check_passed] сегодня в 10:01:44.
ЧЕСТНАЯ ГРАНИЦА: живьём подтверждён ТОЛЬКО Telegram (за 3 суток трафик только он, 749 записей MessageLog). iMessage/BlueBubbles, WhatsApp, Signal, общий voiceHandler, voiceToNote — идентичный код с тем же resolveMessengerTranscriptionIdentity и durable-контекстом, но на проде не исполнялись: это код, а не факт. Тест web425.voiceIdentity.test.ts подменяет транскрайбер через transcribeImpl — зелёное на фикстурах, в зачёт не бралось.
ДВЕ НАХОДКИ ВЫНЕСЕНЫ ОТДЕЛЬНО (не держат этот тикет): embed-роуты без плательщика и settling_at_max по потолку.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-425","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-08-29T10:06:29.807Z