Решили: кооп вошёл в боевой скоуп. Роль узла (Solo / Host / Guest) выведена из транспорта за швом IBattleAuthority и спрашивается каждый кадр, а не решается регистрацией: боевой скоуп поднимается на буте, в главном меню, когда сети ещё нет. Регистрируются обе половины раздачи у всех; расходятся узлы в рантайме. У гостя CombatLoopService не тикает симуляцию вовсе и не снимает кадров — он держит только требуемое отставание показа, а ленту наполняет TapeIntake.

Почему так, а не «хосту стример, гостю приёмник» при сборке контейнера: в момент Configure роли не существует. Единственная альтернатива — пересоздавать боевой скоуп на входе в сессию, но он персистентный по конституции persist-мира (бой начинается командой в живую симуляцию, а не загрузкой сцены), и снос скоупа означал бы снос мира.

Пауза переехала на общий путь. Интент уходит в BattleControlRelay (владелец флага), применяет его NetPauseBridge через TimeScaleService. BattleInputController больше не трогает время сам и читает состояние у релея, а не у времени: интент, посчитанный от чужого состояния, оказывается «уже в этом состоянии» и теряется молча. На смене фазы боя сбрасываются оба владельца сразу.

Состав боя поехал отдельным каналом (BattleRoster, номер 7). В снимках его нет намеренно — определение юнита, команда и арт за бой не меняются, а слать их тридцать раз в секунду значит платить за неизменное. В соло реестр наполняет событие спавна; у гостя спавнов нет, и без объявления лента доезжала бы, а рисовать было бы нечего.

Отставание у гостя постоянное, без оглядки на фазу. Лента гостя пополняется тогда и только тогда, когда у хоста идёт бой: вне боя фронт хостовой ленты стоит на месте (кадр перезаписывает один и тот же тик), и чанки не уходят. Значит растущая лента и есть «бой идёт». Лаг тот же, что у хоста: хост показывает фронт − окно, и гость, идущий вплотную к своему фронту, увидел бы бой на десять секунд раньше напарника.

Грабли: распознавание канала в NetEnvelope было рукописным списком рядом с перечислением. Новый канал завели, в список не дописали — сообщения уходили и молча отбрасывались на приёме; симптом «у гостя не появились юниты», причина за два файла оттуда. Список заменён таблицей, которая строится из самого перечисления, и накрыт тестом на все каналы разом.

Вторая: RuntimeUnit.Id назначает фабрика, а не симуляция. Юнит, собранный в тесте руками, приходит с нулевым id — два таких юнита дают одну запись в реестре на двоих, и падает это не как «паспорт не доехал», а как «команда не та».

Владелец правды: Core/Net/IBattleAuthority.cs, Net/NetBattleAuthority.cs, Net/Tape/BattleRosterRelay.cs, Game/Services/NetPauseBridge.cs, Game/CombatLifetimeScope.cs (RegisterCoop). Тесты CoopBattleWiringTests (хост считает и раздаёт, гость приходит к тому же исходу; соло и гость не раздают), NetPauseBridgeTests (один путь применения паузы), NetEnvelopeTests (каждый объявленный канал узнаётся конвертом).