Решили: запись в забег идёт через корневой роутер SessionCommandRouter (IRunCommands), а не
через шину сессии напрямую. Шина (RunCommandBus) регистрируется в сеансе владельца только собой,
так что дорога к записи одна для всех. Бой и мероприятие берут чтение (IRunStateView) и запись
(IRunCommands) — оба типа существуют всегда, а сеанс под ними меняется.
Сказано: «Ты же ведь учла то что обсуждали. что Занятия могут быть вне сессии? Бои вне сейва (тестовые), отдельные быстрые PVP режимы вне сейва и т.д. Это учтено? Будет работать нормально?» (02.08.2026, сессия f24f0cc4). Вопрос вскрыл дефект, который переезд состояния только что внёс.
Почему: канон уровней требует прямо — «боевой скоуп собирается без RunState, без сети, без
карты», потому что бой заказывают четверо, и трое из них живут без сейва (Ристалище, PvP, тест;
плюс дев-арена). После переезда состояния в сессию BattleStartup и DeploymentController
продолжали просить в конструкторе RunStateService и IRunCommands — то есть владельческую
половину. Скоуп такого боя не поднялся бы вовсе: у гостя этих регистраций нет по построению, а вне
сеанса их нет ни у кого. Код вызывающих при этом давно был готов к отсутствию (_commands?. и
проверка результата) — не готов был контейнер.
Автосейв ушёл из расстановки в запись. _runStates.Autosave() стал _commands.RequestSave():
кто владелец сейва и есть ли он — не дело расстановки, а в коопе гость этим же вызовом попросит
хоста. Реализация — RunCommandApplier.Save(): сохранение не команда и в лог не идёт, но трогает
состояние, а туда ходят только оттуда.
Грабли: резолвить в роутере тот же интерфейс, который он собой и представляет, — рекурсия.
SessionHost.Commands спрашивает у сеанса конкретный RunCommandBus: запрос IRunCommands ушёл бы
вверх (в сессии своей регистрации нет) и вернулся бы к роутеру же.
Что осталось владельческим осознанно: мероприятие (ActRunner, RewardPresenter,
ShopController, NodeResolver, EventEffectApplier) держит RunStateService напрямую. Это
законно: забег ведёт владелец, а гость подключается к чужому мероприятию — его состав собирать
кооп-вертикали. Бой же обязан собираться у всех, поэтому отвязан именно он.
Владелец правды: Game/Session/SessionCommandRouter.cs, Guild/Commands/IRunCommands.cs,
тесты SessionScopeTests.BattleScope_NeverAsksForTheOwnerHalf и
CommandRouter_AnswersNotWrittenWithoutASession.