Решили: подъём сессии, вход к другу и приём назначенного номера (StartHost, Connect, SetLocalPeerId) живут в INetTransport, а комната — за ICoopLobby. CoopSession больше не знает ни Steam-транспорта, ни Steam-лобби.

Почему: пока сеанс держал их конкретными типами, вся гостевая половина коопа — подключение, разрыв, приём приглашения — проверялась ровно одним способом: живым прогоном на двух машинах. То есть тем самым, который трижды подряд пропускал дефекты. Теперь сеанс проходит весь путь на петле в одном процессе.

Игроку это ничего не добавляет: вход к другу по-прежнему один — приглашение платформы (решение Макса 02.08.2026). Без Steam кнопки приглашения честно гаснут, LoopbackLobby никого не зовёт и прямо об этом говорит.

Грабли, стоившие трёх прогонов: суженное ПОЛЕ не сужает ИНЪЕКЦИЮ. Я поменяла тип поля на INetTransport, а параметр конструктора оставила SteamNetTransport. Это компилируется молча — конкретный тип реализует интерфейс, — и VContainer продолжал подавать Steam-транспорт даже там, где в контейнере выбрана петля. Симптом: уход напарника доходил до транспорта, но сеанс о нём не знал, потому что слушал ДРУГОЙ объект.

Нашлось это только диагностикой в два уровня: тест отдельно слушал сам транспорт и отдельно сеанс. Без такого разделения провал читался как «событие где-то потерялось» — и искать его пришлось бы в петле, где всё было исправно. Правило на будущее: меняя тип поля на шов, проверь параметр конструктора — компилятор об этом не скажет.

Владелец правды: Net/Transport/INetTransport.cs, Net/Session/ICoopLobby.cs, Net/Session/CoopSession.cs (комментарий при конструкторе), тест CoopDisconnectScenarioTest.