> Исторический аудит 05.10.2026. Ниже сохранены исходные 44 замечания и SHA-ссылки; они не описывают автоматически текущее состояние. Реализация выбранных пунктов: [переработка chat-agent](</experiments/voice-ai/archive/w/DOCS/CHAT_AGENT_REWORK.md>), [C1](</experiments/voice-ai/archive/w/DOCS/CHAT_AGENT_EXECUTION_C1.md>), [C2](</experiments/voice-ai/archive/w/DOCS/CHAT_AGENT_AUDIO_C2.md>), [приёмка C3 и до/после](</experiments/voice-ai/archive/w/DOCS/CHAT_AGENT_ACCEPTANCE_C3.md>).

## Состояние выбранной работы на 06.10.2026

| Группа | Реализовано | Граница результата |
|---|---|---|
| 10–12: очереди, deadline, отмена | C1: bounded fair Agent/Ruby/LLM queues, 120 с, отмена и request snapshot | Состояние попыток в памяти; durable execution после рестарта не добавлено |
| 16–21: модель и динамичность | C1: dialog_step, native schema/tools, ограниченный tool loop, потоковый текст, фильтр сессии/поколения | Обычный ход — одна генерация; общий браузерный NATS account и user123 сохранены |
| 22–24, 27–29: голос | C2: два ingress, единый FIFO, admission 3, bounded PCM, flush 250 мс, end/abort и контроль final | C3 поставлен: 12 RTC и 5 WSS, общий лимит 3; namespace chat-agent |
| 40: владелец медиасессии | Один Core и один экземпляр каждого ingress | Горизонтальное масштабирование между владельцами отложено |
| 44: private ICE adapter | Изолирован, aiortc 1.14.0/aioice 0.10.2, реальные Trickle/TURN/DTLS/PCM tests | Сопровождение private API остаётся нашей ответственностью |
| 25, 26, 30: GPU/decoder/длинная фраза | Не изменялись | Отдельная работа владельца voice/H100 |

Изоляция отображаемых событий не является проверкой личности владельца UUID-сессии. Пункты безопасности, не вошедшие в согласованный план, остаются открытыми. Результаты C0/C1/C2/C3 — короткие серии синтетической речи; они не доказывают WER/DER для реальных разговоров.

---

# Возможные исправления и улучшения голосового чата

Дата аудита: 05.10.2026. Статус: предложения для обсуждения и выбора; изменения не реализованы этим аудитом.

Документ сохраняет результат read-only аудита. Наблюдения о runtime относятся к моменту проверки, а предлагаемые измеримые результаты являются критериями будущей приёмки. Это не утверждённый план работ и не подтверждение новых нагрузочных или аудиотестов.

Ссылки на код закреплены на полных commit SHA проверенных версий:

- webrtc-komaroff: `d2cf68cad3e784c36fede96f39237e5d27db8184`.
- py_faster_whisper: `ce9e96784f2e5018afa48264fcb75f0dbfc71d6e`.

Для открытия исходников требуется доступ к корпоративному GitLab. Ссылки не зависят от локальных каталогов или временных checkout.

Главные проблемы сейчас — **изоляция пользователей, права браузера в NATS, повторное выполнение запросов и восстановление после сбоев**. Затем идут очереди Agent/LLM, обработка обрывов аудио и достоверность статусов.

Нашёл **44 замечания**. Среди них есть подтверждённые ошибки, архитектурные ограничения и риски, проявление которых ещё нужно измерить. Называть их все «44 багами» было бы неправильно.

Во время аудита код, конфигурацию и runtime не менял. Проверял актуальные исходники `webrtc-komaroff d2cf68ca` и `py_faster_whisper ce9e9678`, тесты, DRS и текущие контейнеры через Insight. В chat запущены 13/13 сервисов, в voice — 16/16. Выбран `ruby_node`, live ASR работает в режиме `streaming`. Проверенные Core, Agent, LLM и workflow соответствуют развёрнутому коду.

Новые аудиозапросы, файловые задачи и нагрузочные тесты не запускал. H100 открыл форму авторизации, поэтому его текущую загрузку и состояние GPU не подтверждаю. Ниже измеримые результаты — **критерии приёмки предлагаемых изменений**, а не обещание уже достигнутого ускорения.

## 1–7. Безопасность и разделение пользователей — исправлять первыми

**1. История чата не проверяет владельца сессии.**  
Сервер проверяет UUID, затем читает историю только по `session_id`. Авторизация на входе подтверждает, что человек вошёл, но приложение не проверяет его право на конкретный чат. Общий `USER_ID` задан как `user123`. Для нескольких независимых пользователей это блокирующий риск. [Код выборки истории](</experiments/voice-ai/archive/w/src_agent/utils/db.py#L187>).

Исправление: привязать сессии к проверенной личности из Slim Auth и проверять владельца при чтении, отправке, редактировании и голосовом подключении. **Результат:** все попытки пользователя А обратиться к сессии Б получают отказ.

Это соответствует классу ошибок [OWASP Broken Object Level Authorization](<https://api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/>). Случайный UUID такую проверку не заменяет.

**2. Файловые задачи имеют ту же проблему владения.**  
Получение результата использует `job_id`; запуск этапа — тоже `job_id`; сброс и удаление — переданный клиентом `session_id`. Проверки принадлежности вошедшему пользователю нет. [Получение результата](</experiments/voice-ai/archive/y/src/webui/server.py#L603>), [сброс сессии](</experiments/voice-ai/archive/y/src/webui/server.py#L1024>).

Исправление: хранить владельца задачи и проверять его на каждом API. **Результат:** чужую транскрипцию нельзя прочитать, перезапустить или удалить, даже зная её идентификатор.

**3. Браузеру выдаются общие сервисные credentials NATS.**  
Chat получает их через `settings.js`, voice — через HTML. В описанном NATS account нет ограничения subjects для этого пользователя. Получается, интерфейсу дают права значительно шире получения собственных событий. [Выдача credentials](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/docker/front/nginx/50-settings.sh#L26>), [настройка account](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/stack/webrtc.drs#L227>).

Минимальный вариант: отдельный браузерный пользователь с узкими правами. Предпочтительный: серверный шлюз событий, который выдаёт только события владельца. **Результат:** браузер не может публиковать служебные команды и подписываться на чужие subjects. Такой контроль поддерживается штатными [permissions NATS](<https://docs.nats.io/learn/security/authorization>).

**4. Ответы и команды разных сессий могут смешиваться в интерфейсе.**  
Frontend подписан на общий `nats.events.user123`. Для ASR есть фильтрация, но обработчики `message`, `thought` и `client` не проверяют принадлежность сессии. Поэтому защита реализована неодинаково для разных событий. [Подписка](</experiments/voice-ai/archive/w/src_front/src/config/appConfig.ts#L49>), [обработчики](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_front/src/assistant/assistantApp.ts#L339>).

Исправление: разделить доставку на сервере и ввести единый обязательный envelope событий. **Результат:** две одновременные сессии не получают ни одного чужого ответа, статуса или действия карты.

**5. В логирование всё ещё могут попадать credentials и содержимое запросов.**  
В voice URL уже убрали из стартовых сообщений. Но LLM пишет полный `_nats_url` при подключении, публикует вход LLM в debug-событие, а браузер логирует события до фильтрации сессии. [Подключение и debug](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/utils/base_service.py#L46>), [логирование frontend](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_front/src/assistant/assistantApp.ts#L307>).

Исправление: убрать URL с credentials, отключить содержимое промптов по умолчанию, централизовать редактирование секретов. **Результат:** ноль секретов и чужих текстов в штатных логах и browser console.

**6. Ограничены отдельные сообщения, но не количество дорогих запросов.**  
Есть лимит HTTP body и ICE-кандидатов. Однако нет достаточного ограничения числа peer connections и запросов на пользователя до выделения ресурсов. [Реестр подключений](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/handlers/handle_offer.py#L99>).

Исправление: лимиты подключений, длины текста, очереди и частоты запросов; отказ до создания peer и запуска LLM. **Результат:** ресурсы ограничены заданным пределом, перегрузка возвращает понятный `429/503`, действующие пользователи продолжают работать. Это отдельный риск [OWASP Resource Consumption](<https://api-security.owasp.org/editions/2023/en/0xa4-unrestricted-resource-consumption/>).

**7. Жизненный цикл WebSocket не связан полностью с авторизацией пользователя.**  
В NATS-конфигурации нет явного allowlist `Origin`. OAuth проверяет handshake; в приложении не видно механизма закрытия уже открытых каналов после выхода или истечения пользовательской сессии. ICE-токен защищает конкретное подключение, но не заменяет отзыв пользовательского доступа. [NATS WebSocket](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/stack/webrtc.drs#L222>), [ICE-канал](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/handlers/handle_ice.py#L139>).

Исправление: разрешённые origins, короткоживущие права и отзыв каналов при logout. **Результат:** чужой origin отклоняется, отозванная сессия прекращает получать данные за установленное время. Возможность эксплуатации текущего ingress отдельно не проверял. Основание — [OWASP WebSocket Security](<https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html>).

## 8–16. Выполнение запросов и сохранение состояния

**8. Конфликт состояния повторно запускает весь сценарий.**  
Workflow вызывается до сохранения версии состояния. При конфликте Agent повторяет workflow, создавая новый внутренний `request_id`. Уже выполненные LLM/tool вызовы повторяются. [Код повторов](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_agent/agent.py#L122>).

Исправление: последовательная обработка внутри одной сессии, стабильные ключи идемпотентности и журнал выполненных действий. **Результат:** повторная доставка и искусственный конфликт создают ровно одно действие и один итоговый ответ. Сейчас инструменты преимущественно читают demo-данные; для будущих записывающих инструментов риск станет существенно выше.

**9. Сохраняется итог сценария, но не его промежуточные шаги.**  
Результаты внешних вызовов находятся в памяти `GraphRunner`. После падения процесса продолжить с последнего завершённого шага нельзя. [GraphRunner](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/graph_runner.rb#L13>).

Исправление: checkpoints после шагов и сохранённые результаты инструментов. **Результат:** принудительный restart между шагами продолжает задачу без повторения уже завершённого действия.

**10. Таймаут не означает отмену работы ниже по цепочке.**  
Текущие ожидания — Core 600 секунд, Agent 270, отдельный workflow-вызов 120. Само соотношение чисел не является ошибкой. Проблема — отсутствие общего deadline и распространяемой отмены: вызывающий сервис уже может закончить ожидание, а нижний продолжает работать. [Agent](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_agent/agent.py#L212>), [workflow transport](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/runtime_io.rb#L62>).

Исправление: передавать deadline и cancellation по всей цепочке. **Результат:** после отмены не начинаются новые инструменты; фоновые задачи освобождаются в заданный срок.

**11. Один долгий запрос задерживает следующие запросы Agent.**  
NATS callback ждёт весь `runner.run()`. Обработка этой подписки последовательная. [Обработчик Agent](</experiments/voice-ai/archive/w/src_agent/service.py#L153>). Такой порядок подтверждается реализацией [nats-py subscription](<https://nats-io.github.io/nats.py/_modules/nats/aio/subscription.html>).

Исправление: ограниченный пул обработчиков с последовательностью внутри одной сессии. **Результат:** медленный сценарий пользователя А не блокирует короткий запрос пользователя Б.

**12. Semaphore LLM ограничивает выполнение, но не очередь.**  
На каждое сообщение создаётся task; дальше он ждёт semaphore. Число ожидающих задач отдельно не ограничивается. [Код LLM](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/utils/base_service.py#L98>).

Исправление: ограниченная очередь, проверка deadline перед генерацией, управляемая параллельность. **Результат:** длина очереди никогда не превышает установленный предел; просроченные запросы не расходуют GPU.

**13. Сохранённый ответ может не дойти до интерфейса.**  
Сначала завершается DB transaction, затем публикуются UI-события, затем отправляется reply. Сбой между этими операциями оставляет сохранённый ответ без гарантированной доставки. [Порядок операций](</experiments/voice-ai/archive/w/src_agent/service.py#L164>).

Исправление: transactional outbox — записывать событие доставки в той же транзакции; клиенту дать восстановление по cursor и удаление дубликатов. **Результат:** падение после DB commit не теряет ответ и не создаёт повторное отображение.

Core NATS предоставляет доставку [at-most-once](<https://docs.nats.io/learn/core-nats/>). Для живых аудиокадров это подходящий компромисс; итоговые ответы и принятые задачи требуют отдельной гарантии.

**14. Принятая пользовательская реплика сохраняется слишком поздно.**  
Она записывается после выполнения workflow. Если процесс упадёт раньше, в истории нет надёжной записи принятого запроса. Некоторые штатные ошибки сохраняются как ответы, но это не покрывает падение до транзакции. [Сохранение реплик](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_agent/agent.py#L138>).

Исправление: сначала сохранять принятую реплику и статус выполнения. **Результат:** каждый подтверждённый запрос имеет запись `pending/completed/failed`, включая аварийно прерванные.

**15. Редактирование разрушает ветку истории, а ошибка оставляет UI переписанным.**  
DB удаляет сообщения начиная с редактируемого; frontend меняет историю до ответа сервера и не восстанавливает её при отказе. Память workflow учитывает редактирование, но полноценной сохранённой ветки нет. [Удаление](</experiments/voice-ai/archive/w/src_agent/utils/db.py#L127>), [frontend](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_front/src/assistant/assistantApp.ts#L471>).

Минимум: откат UI при ошибке. Далее — версии реплик или ветки. **Результат:** неуспешное редактирование сохраняет прежний чат; успешное позволяет восстановить предыдущую версию.

**16. Динамичность ограничена тремя заранее заданными сценариями.**  
Активный Ruby runtime регистрирует FreeSpeech, WhereMyFlight и FindNearestAirport. LLM выбирает сценарий, но не строит произвольный план из разрешённых инструментов. [Реестр](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_langgraph_rb_node/lib/src_langgraph_rb_node/scenarios/built_in_registry.rb#L12>).

Это архитектурное ограничение, а не автоматически плохая практика. Расширять лучше постепенно: сохранить надёжные сценарии и добавить ограниченный tool loop. **Результат:** новые сочетания инструментов выполняются без отдельного сценария, при соблюдении бюджета шагов и времени.

## 17–21. Скорость и предсказуемость LLM

**17. Обычный разговор платит за дополнительный LLM-router.**  
Перед генерацией ответа выполняется генеративный выбор сценария. Для простых запросов это дополнительная очередь и inference. [Router](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/router.rb#L31>).

Исправление: измерить простой быстрый маршрут или отдельный лёгкий классификатор. **Результат:** типичный small talk требует одного генеративного вызова; точность выбора инструментальных сценариев не ухудшается на эталонных запросах.

**18. Ошибка router превращается в свободный разговор.**  
При неуспешном LLM-ответе выбирается `free_speech`, после чего возможен ещё один LLM-вызов. Недоступность сервиса смешивается с отсутствием подходящего намерения. [Fallback](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/router.rb#L32>).

Исправление: различать ошибку транспорта, неверный формат и неизвестное намерение. **Результат:** outage даёт понятный отказ без лишней повторной генерации.

**19. Лимит ответа не применяется к удалённому Ollama через текущую настройку.**  
`max_tokens` извлекается из options, но remote-вызову не передаётся как `num_predict`. Настройка `LLM_CONTEXT_SIZE` тоже не устанавливает `num_ctx` в этом запросе. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/service.py#L725>).

Исправление: явно отображать настройки в параметры provider. **Результат:** отправляемый запрос содержит заданные лимиты, число выходных токенов контролируется ими. Названия параметров подтверждены [документацией Ollama](<https://docs.ollama.com/modelfile>).

**20. JSON Schema передаётся как инструкция, а не как ограничение генерации.**  
Схема вставляется в prompt, но `format` в Ollama request отсутствует. Проверка после генерации есть, однако модель сначала может выдать неподходящий формат. [Формирование prompt](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/service.py#L712>), [request](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/clients/ollama_chat.py#L12>).

Исправление: использовать native structured output, сохранив серверную валидацию. **Результат:** измеримое снижение schema errors и повторных вызовов. Возможность есть в [Ollama API](<https://docs.ollama.com/api/chat>).

**21. Пользователь ждёт полный ответ модели.**  
Запрос использует `stream: false`; итог показывается после завершения генерации. Потоковая транскрипция уже есть, а потоковой генерации ответа нет. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_llm/clients/ollama_chat.py#L19>).

Исправление: передавать текст ответа частями; команды инструментов выполнять после полной проверки. **Результат:** отдельно измеряется и сокращается время до первого видимого фрагмента. Полное время генерации при этом может остаться прежним.

## 22–32. Голосовой тракт

**22. Frontend ждёт восстановления сети, а Core уже закрывает peer.**  
Frontend допускает восемь секунд состояния `disconnected`; сервер считает его терминальным и закрывает соединение сразу. [Frontend](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_front/src/webrtc/session.ts#L77>), [Core](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/utils/pc_lifecycle.py#L26>).

Исправление: согласовать grace period, затем добавить ограниченный ICE restart/reconnect. **Результат:** короткий обрыв и смена сети восстанавливаются без reload. Такой механизм предусмотрен, например, в [LiveKit](<https://docs.livekit.io/intro/basics/connect/>).

**23. После ошибки публикации PCM распознавание может навсегда остановиться внутри подключённого peer.**  
`WhisperStreamNode` ставит `_broken=True` и больше не отправляет кадры. Сам media connection при этом может оставаться установленным. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/processors/whisper_stream_node.py#L125>).

Исправление: явное состояние деградации и восстановление с новым `stream_id`. **Результат:** после сбоя NATS либо распознавание восстанавливается, либо интерфейс требует переподключение; бесконечного «подключён, но не распознаёт» нет.

**24. Девятый поток может получить молчаливый отказ.**  
Bridge при восьми активных streams просто возвращает управление при поступлении нового. Peer уже может быть подключён. [Лимит](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/src/streaming_asr/bridge.py#L124>).

Исправление: проверять свободное место до подключения или отправлять явный отказ. **Результат:** лишний пользователь получает «сервер занят», а не бесконечное ожидание транскрипции.

**25. GPU decode выполняется одним последовательным циклом.**  
Это безопасно для памяти, но один долгий decode задерживает другие фразы. Наличие восьми слотов ещё не доказывает способность обслужить восемь говорящих одновременно. [Scheduler](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/src/streaming_asr/worker.py#L166>).

Исправление: измерить нагрузку, затем выбирать batching или управляемую параллельность. **Результат:** известен реальный предел потоков при обработке быстрее поступления аудио; p95 задержки измерен для 1/4/8 клиентов.

**26. Зависший decoder может продолжать выглядеть здоровым.**  
Health проверяет наличие decode loop и NATS. Сам decode не имеет отдельного deadline. Особенно проблемна зависшая финальная фраза. [Decode](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/src/streaming_asr/worker.py#L182>), [health](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/src/streaming_asr/worker.py#L247>).

Исправление: deadline inference и watchdog с возможностью восстановления worker. Простая отмена `to_thread` не гарантирует остановку вычисления. **Результат:** искусственное зависание обнаруживается за установленное время и не блокирует последующие фразы бесконечно.

**27. Каждый 20-мс аудиокадр ждёт NATS flush.**  
Получается около 50 подтверждающих обменов на поток в секунду. При заметном RTT это может ограничить темп публикации. В текущей сети влияние не измерял. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/processors/whisper_stream_node.py#L114>).

Исправление: контролируемая пакетная отправка или более редкий flush с сохранением ограниченной очереди. **Результат:** меньше flush-операций, отсутствие роста очереди при заданном RTT, без ухудшения задержки речи.

**28. Неполный PCM-чанк дополняется тишиной вместо накопления остатка.**  
Каждый resampled frame разбивается отдельно; короткий остаток дополняется нулями. При иной пакетизации это способно добавлять лишнюю тишину и менять длительность. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/processors/track_source_node.py#L153>).

Исправление: общий Audio FIFO между входными frames. **Результат:** одинаковое аудио при пакетизации 10/20/60 мс даёт одинаковую длительность PCM; padding допускается только на окончательном хвосте.

**29. Фоновые задачи audio graph контролируются неполностью.**  
Graph создаёт стартовые tasks; узлы создают собственные tasks; финальные транскрипции запускают Agent отдельными tasks. Единого владельца их жизненного цикла нет. [Graph](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/processors/graph.py#L34>), [final callback](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/processors/whisper_stream_node.py#L166>).

Исправление: supervision и явное владение задачами. **Результат:** падение узла переводит поток в ошибку, disconnect освобождает связанные задачи и подписки. Для этого подходят механизмы [structured concurrency Python](<https://docs.python.org/3/library/asyncio-task.html>).

**30. Длинная нестабильная фраза может упереться в восьмисекундное окно.**  
Окно сокращается после совпадения слов двух decode. Если устойчивый префикс не появляется, дальнейший рост приводит к overflow. Тест длинной фразы существует, но использует символический decoder, а не реальный Whisper. [Окно](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/src/streaming_asr/worker.py#L138>), [тест](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/tests/test_streaming_asr.py#L273>).

Исправление: проверить реальные монологи и определить безопасное разделение длинных фраз. **Результат:** записи 30/60 секунд проходят без потери или дублирования текста и без неожиданного overflow.

**31. При включённом browser VAD передача звука зависит от отрисовки страницы.**  
Energy gate обновляется через `requestAnimationFrame`. В фоне такие callbacks могут приостанавливаться, оставляя gate в последнем состоянии. По умолчанию этот VAD выключен. [Код](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_front/src/audio/vad.ts#L32>).

Исправление: перенести обработку в AudioWorklet или использовать browser VAD только как индикатор. **Результат:** свёрнутая вкладка и загруженный UI не меняют прохождение речи. Причина подтверждается поведением [requestAnimationFrame](<https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/requestAnimationFrame>).

**32. Часть аудионастроек меняет UI, но не текущий поток.**  
AEC/NS и включение VAD записываются в объект настроек. Они применяются при создании следующего соединения; текущему track новые constraints не передаются. [Настройки](</experiments/voice-ai/archive/w/src_front/src/audio/audioState.ts#L22>).

Исправление: применять поддерживаемые constraints сразу либо явно обозначать необходимость переподключения. **Результат:** состояние controls соответствует фактическому `getSettings()` действующего track.

## 33–37. Файловое распознавание и диаризация

**33. Лимит загрузки не контролируется непосредственно в streaming upload.**  
Настроен `client_max_size`, но цикл `read_chunk()` пишет файл без собственного счётчика суммарных байтов. Для потокового чтения полагаться только на эту настройку недостаточно; дополнительные ограничения ingress не подтверждены. [Upload](</experiments/voice-ai/archive/y/src/webui/server.py#L698>).

Исправление: явный счётчик, лимит длительности загрузки и дисковая квота. **Результат:** превышение установленного размера стабильно возвращает `413`, включая chunked upload.

**34. Запись файлов и JSON выполняется синхронно в async-сервисе.**  
Дисковые операции могут задерживать HTTP и обновление статусов при медленном хранилище. Реальный размер задержки не измерен. [Upload write](</experiments/voice-ai/archive/y/src/webui/server.py#L704>), [state write](</experiments/voice-ai/archive/y/src/webui/file_mode.py#L708>).

Исправление: вынести блокирующие операции из event loop. **Результат:** загрузка большого файла не ухудшает p95 времени ответа health/status сверх принятого бюджета.

**35. Блокировка файловых задач может потеряться, пока задача продолжает работу.**  
Lock хранится в памяти сервиса. Restart забывает владельца. Heartbeat-клиент не проверяет `refreshed`; транспортная ошибка может остановить heartbeat task, не останавливая вычисление. [Lock service](</experiments/voice-ai/archive/y/src/webui/job_lock_server.py#L119>), [heartbeat](</experiments/voice-ai/archive/y/src/webui/job_lock.py#L80>).

Исправление: проверяемая lease, сохранённый владелец и fencing — номер поколения, запрещающий старому владельцу продолжать работу. **Результат:** restart lock и потеря heartbeat не допускают одновременного выполнения двух защищённых задач.

**36. JSON-состояния и benchmark-файл переписываются неатомарно.**  
Используется обычный `write_text`. Падение посередине записи может оставить повреждённый файл; часть чтений превращает это в отсутствие результата. [State](</experiments/voice-ai/archive/y/src/webui/file_mode.py#L708>), [benchmark](</experiments/voice-ai/archive/y/src/webui/bench.py#L491>).

Исправление: временный файл и atomic rename; для постоянных результатов предпочтительна DB transaction. **Результат:** аварийное завершение во время записи оставляет предыдущую либо новую целую версию.

**37. Срок хранения результатов недостаточно очевиден пользователю.**  
Обычные file jobs удаляются примерно через 30 секунд после завершения; benchmark store оставляет последние 1000 записей. Постоянный volume сам по себе не означает бессрочное хранение всех результатов. [Cleanup](</experiments/voice-ai/archive/y/src/stt_whisper_to_nats.py#L1491>), [benchmark retention](</experiments/voice-ai/archive/y/src/webui/bench.py#L511>).

Исправление: явная политика хранения, экспорт и отдельное постоянное хранилище. **Результат:** результат доступен весь объявленный срок; создание 1001-й записи не удаляет данные вопреки обещанной политике.

## 38–44. Проверки, эксплуатация и сопровождение

**38. CI voice не запускает весь важный набор тестов.**  
Обязательная job запускает четыре streaming-модуля. Тесты загрузок, диагностики, model transport и диаризации существуют, но не входят в этот gate. [CI](<https://git.giscloud.ru/voice-chat/py_faster_whisper/-/blob/ce9e96784f2e5018afa48264fcb75f0dbfc71d6e/.gitlab-ci.yml#L31>).

Исправление: включить подходящие тесты затронутых компонентов; добавить проверки нескольких пользователей и отказов. **Результат:** намеренно внесённая регрессия upload/lock/diagnostics блокирует публикацию.

**39. Качество распознавания и диаризации не доказано эталонными метриками.**  
Benchmark измеряет время и `quality_proxy`; интерфейс честно предупреждает, что это не WER/DER. Успешный ответ на «2+2» не подтверждает качество на шуме, фамилиях и пересекающейся речи. [Метрики benchmark](</experiments/voice-ai/archive/y/src/webui/bench.py#L617>).

Исправление: размеченный набор записей. **Результат:** известны ошибки слов и символов, ошибки определения говорящего, ложные начала и обрывы фраз. Смена модели принимается по этим метрикам вместе со скоростью.

**40. Core нельзя просто масштабировать до двух replicas.**  
POST offer создаёт ICE session в памяти одного процесса. Следующий WebSocket должен попасть туда же. При обычном распределении запросов он может прийти в другой экземпляр. [Registry](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/handlers/handle_ice.py#L18>).

Исправление: маршрутизация к владельцу соединения или media platform с готовым механизмом. **Результат:** 100 подключений через две replicas проходят без ошибок «connection not found»; обновление учитывает активные звонки.

**41. Основной chat-контур имеет единичные точки отказа.**  
Insight показывает один узел и по одной replica, включая NATS и Postgres. Наличие проверяемых внешних backup/restore-процедур в этом аудите не установлено.

Минимум: резервные копии и восстановление на отдельном окружении. HA добавлять под требуемую доступность. **Результат:** известны и проверены допустимая потеря данных и время восстановления, вместо предположения «volume сохранится».

**42. «Running» и простые health endpoints не подтверждают готовность пользовательского пути.**  
У Core в текущем контейнере нет Docker healthcheck; его index отдаёт текст. WebUI health возвращает `ok` без проверки готовности полного тракта. [Core index](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/handlers/handle_index.py#L4>), [WebUI health](</experiments/voice-ai/archive/y/src/webui/server.py#L1131>).

Исправление: разделить liveness и readiness, отдельно проверять зависимости и периодически — реальный сценарий. **Результат:** потеря критической зависимости снимает readiness за установленный срок, не создавая ненужных restart.

**43. У проверенных контейнеров нет явных ограничений ресурсов и они работают от root.**  
В Insight для Core, LLM и live voice: `Memory=0`, `NanoCpus=0`, `User=""`, root filesystem writable. Это повышает последствия ошибки или перегрузки для соседних сервисов.

Исправление: ресурсные бюджеты, non-root и доступ на запись только к нужным каталогам. **Результат:** превышение бюджета одного сервиса не исчерпывает память всего узла; приложение сохраняет работу без root.

**44. WebRTC зависит от внутренних API библиотек.**  
Trickle adapter меняет поведение приватных объектов aiortc/aioice. Здесь уже сделано многое правильно: изоляция, точные версии и реальные ICE/DTLS/PCM-тесты. Но сопровождение такого слоя остаётся нашей ответственностью. [Adapter](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/d2cf68cad3e784c36fede96f39237e5d27db8184/src_core/utils/trickle_ice.py#L244>).

Минимум: сохранять строгую проверку обновлений. Более крупный вариант — передать этот слой media platform. **Результат:** каждое обновление проверено на TURN и decoded PCM; при замене исчезают собственные приватные ICE-патчи.

## Как я бы исправлял это постепенно

| Вариант | Изменения | Измеримый результат | Цена и ограничения |
|---|---|---|---|
| **А. Минимально инвазивно** | Владение сессиями и jobs; узкие NATS-права; фильтрация событий; редактирование логов; admission limits; исправление Ollama options; расширение CI | Ноль чужих данных и команд; предсказуемый отказ при перегрузке; реально работающие лимиты; регрессии блокируются CI | Сохраняем текущие сервисы и протоколы. Это первый обязательный этап |
| **Б. Сделать выполнение надёжным** | Очередь с последовательностью внутри сессии; идемпотентность; outbox; deadlines; отмена; checkpoints; устойчивые file locks | Один запрос — одно действие; restart не теряет принятые задачи; ответ восстанавливается; отменённая работа прекращается | Добавляются таблицы и состояния выполнения, требуется проверка сбоев |
| **В. Заменить отдельные сложные слои** | Media слой на self-hosted LiveKit; сохранить NATS/ASR; оставить один основной workflow runtime; добавить ограниченный tool loop | Восстановление сети, работа нескольких replicas, меньше собственного ICE-кода; новые комбинации инструментов без отдельного сценария | Новая инфраструктура и миграция. Ускорение подключения само по себе не гарантируется |
| **Г. Постепенно переписать основу** | Единый сервис диалогов и пользовательского API; отдельный media слой; GPU workers; постоянные jobs; типизированные инструменты и события | Устраняются повторные реализации состояния и таймаутов; единые правила доступа; воспроизводимые нагрузочные и аварийные тесты | Максимальная стоимость. Переносить по одному пути с возможностью возврата |

Для долгих многошаговых задач можно рассмотреть Temporal, когда собственный журнал шагов станет слишком сложным. Он предоставляет [durable execution](<https://docs.temporal.io/temporal>), но **не отменяет необходимость идемпотентности инструментов**: [Activity может выполняться повторно](<https://docs.temporal.io/activity-definition>).

Я бы начал с **варианта А**, затем сделал **Б**. После этого замерил реальные задержки и выбрал, какие части заменять по варианту В. Полное переписывание имеет смысл, если измерения покажут, что поддержка собственной инфраструктуры обходится дороже миграции.

Есть ещё два продуктовых ограничения: текущий голосовой путь даёт текстовый ответ и блокирует новую речь на время обработки; полноценного TTS и перебивания ответа нет. Авиационные инструменты используют demo-данные. Это не нарушения лучших практик само по себе, но их нужно учитывать при описании возможностей продукта.

Предыдущее ускорение ICE сохраняет смысл: в [приёмке 05.10](</experiments/voice-ai/archive/w/DOCS/WEBRTC_TRICKLE_ACCEPTANCE_2026-10-05.md#L3>) медиана подключения в одной сети снизилась с **16,108 до 1,245 секунды**. Следующий полезный замер — разложить время **от конца речи до первого фрагмента ответа** на ожидание окончания фразы, ASR, очередь Agent, router и генерацию LLM. Это позволит выбирать дальнейшие правки по реальному эффекту.
