Решили: мероприятие (забег, а дальше Двор, Ристалище, PvP, дев-арена) получает свой скоуп. ActivityHost в корне рождает его на входе и хоронит на выходе; в него уехали петля акта, узлы, награды, передышка, магазин, последствия ивентов, рукопожатие боя и владелец боевого скоупа. Верхняя петля игры (GameFlow) осталась в корне и теперь только открывает занятие, ведёт его и закрывает.

Почему раньше Сессии: критерий фазы Сессии — «RunStateService уезжает вниз» — недостижим, пока забеговый флоу живёт в корне: контейнер смотрит вверх, и корневые потребители перестали бы видеть состояние. Зависимость идёт снизу вверх (решение Макса 02.08.2026, порядок фаз 2 и 3 поменян).

Что это дало сразу: из RunActAsync ушли две строки, которыми конец забега объявлялся вручную — RequestReset и SetPhase(None). Теперь забег кончился ⇒ кончилось занятие ⇒ ушли бой, его скоуп и часы. Каждая забытая такая строка раньше делала следующий забег чужим.

Грабли:

  • Компилятор не ловит разъехавшуюся композицию. Регистрации уехали вниз, а корневые потребители (UiRootBootstrap, UiNavigator, RunAudioPresenter) продолжали инжектить IBattleClock — все 23 сборки при этом зелёные, потому что ошибка живёт в контейнере, а не в типах. Ловится только грепом по потребителям и прогоном.
  • Часам понадобился роутер — тот же приём, что у кадра показа (StageFrameRouter): ActivityClockRouter в корне делегирует часам текущего занятия, а вне занятия отвечает «боя нет». Прямая ссылка означала бы фазу мероприятия, которое давно кончилось. Событие смены фазы он переподписывает сам: роутер вечен, часы под ним меняются.

Владелец правды: Game/Activity/ActivityHost.cs, Game/Activity/ActivityInstaller.cs, Game/Activity/ActivityClockRouter.cs.