# Один turn: один результат, атомарная запись и безопасные реплики

> Исторический snapshot. После разделения chat/voice адреса и deployment ownership изменились: чат — https://chat-agent.gis-master.ru/, file UI/diagnostics — https://voice.gis-master.ru/. Текущая поставка: [DEPLOYMENT](</experiments/voice-ai/archive/w/DOCS/DEPLOYMENT.md>). Старые адреса и service owners ниже описывают дату исследования, а не инструкции для текущего runtime.

Статус: предложение, не реализовано. Инвазивность: **Средняя**. Основа: `webrtc-komaroff@772de4a`, 16.09.2026.

Agent сохраняет runtime и историю двумя разными операциями. При ошибке записи общий retry снова вызывает workflow. Между этими вызовами модель и tools уже могли выполнить работу. Кроме того, обе Ruby реализации подписываются на run subject без queue group: при нескольких репликах один publish доходит до каждой.

![Обработка turn и фиксация результата](</experiments/voice-ai/archive/w/DOCS/architecture-2026-09-16/diagrams/p04-turn.svg>)

Queue group решает выбор исполнителя для одного сообщения. Идемпотентность решает повторное поступление того же логического turn. Транзакция решает совместную фиксацию runtime и истории. Эти три свойства нужны одновременно и не заменяют друг друга.

## Предлагаемая правка

Сначала добавить одинаковый queue group на run subject выбранного Ruby runtime. Во время rolling update старый plain subscriber продолжит получать копии, поэтому переход проверить отдельно и не допускать неконтролируемого смешанного режима.

Следом ввести реестр turn с уникальным ключом `(session_id, turn_id, revision)`. Revision отличает повтор доставки от edit той же реплики. Запись хранит request fingerprint, статус и результат. Повтор завершённого turn возвращает сохранённый ответ; несовпадение payload при том же ключе — явная ошибка. Session ownership проверяется до обращения к состоянию.

Workflow не должен исполняться внутри долгой SQL-транзакции с удержанием row lock. Нужна короткая операция claim/lease, выполнение с fencing или session-serialization и короткая финальная транзакция. В ней вместе фиксируются runtime, обе строки истории, результат turn и события outbox.

Публикация outbox выполняется после commit. Она может повториться, поэтому событие получает стабильный event_id, а получатель дедуплицирует его. Для внешнего инструмента тоже нужен idempotency key; outbox сам по себе не отменяет уже совершённый внешний эффект.

## Что проверять

Исходная точка: до **4 workflow executions** при значении `RUNTIME_CONFLICT_RETRIES=3` в одной попытке Agent; фактическая частота UNKNOWN. UI publisher подавляет повторы только внутри своего одного вызова, не между повторными запросами.

Цели для тестов: 100 повторов одного turn дают одну логическую фиксацию и один стабильный ответ; две реплики Ruby исполняют один запрос одним worker; crash после commit до publish не теряет событие; duplicate publish не добавляет вторую реплику в UI. Для external side-effect fixture число effects равно одному, если backend поддерживает ключ; неподдерживаемые backends требуют reconciliation, не обещания exactly-once.

Отдельно проверить два разных turn одной сессии, две сессии одного пользователя, edit/replay и потерю lease. Снять retry ratio, duplicate-effect count, p95 lock/claim wait, время от commit до доставки события.

## Риски и внедрение

Это миграция схемы и протокола. Сначала добавить таблицы и совместимый reader, затем включить новый writer для тестовых сессий. Один turn имеет одного выбранного владельца; dual-write в обе архитектуры недопустим для tools с эффектами. Rollback возвращает маршрутизацию, сохраняя уже записанные новые данные и read compatibility.

## Основания

[Agent retry](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/772de4a714ae6c5abea91946e85a1953ee919ec3/src_agent/agent.py>), [отдельный UPDATE](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/772de4a714ae6c5abea91946e85a1953ee919ec3/src_agent/repositories/runtime_state.py>), [history transaction](</experiments/voice-ai/archive/w/src_agent/utils/db.py>), [Ruby subscription](<https://git.giscloud.ru/voice-chat/webrtc-komaroff/-/blob/772de4a714ae6c5abea91946e85a1953ee919ec3/src_langgraph_rb_node/lib/src_langgraph_rb_node/runtime/service.rb>). [NATS queue groups](<https://docs.nats.io/learn/core-nats/queue-groups>) и [PostgreSQL transactions](<https://www.postgresql.org/docs/16/tutorial-transactions.html>) подтверждают свойства доставки и атомарности. Context7: `/nats-io/nats.docs`, `/websites/postgresql_18`, 16.09.2026; проект использует Postgres 16, поэтому для SQL baseline дана его версия. Outbox — предлагаемая композиция, не найденный компонент проекта.

[Все предложения](</experiments/voice-ai/archive/w/DOCS/architecture-2026-09-16/improvements/README.md>) · [Фактическая архитектура](</experiments/voice-ai/archive/w/DOCS/architecture-2026-09-16/README.md>)
