Решили: подписки экрана живут в OnEnter/OnExit, а не в Build. Экран, успевший резолвнуться внутри собственного Build, в стек не кладётся — навигатор спрашивает у него IsResolved.

Почему: Build вызывается, только пока Root == null, то есть один раз за сессию; показывают и снимают экран сколько угодно раз. Пара «подписка в Build, отписка в OnExit» поэтому асимметрична по своей природе: после первого закрытия подписок не остаётся, а вернуть их некому. Дев-консоль от этого выглядела сломанной — команды выполнялись, но окно молчало; список логов застывал на строках первого показа.

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

Грабли: снятие экрана, которого ещё нет в стеке, проходит вхолостую и никак себя не проявляет. RemoveScreen не находит экран, молча выходит — а Push следом кладёт уже резолвнутый экран и в стек, и в слой. Панель главного меню оставалась лежать поверх мира до ближайшего PopAll. Тот же класс — синхронный колбэк CancellationToken.Register на уже отменённом токене: он отрабатывает ДО того, как навигатор успевает запомнить регистрацию, и запись оседает в словаре навсегда.

Владелец правды: UiScreen.IsResolved<remarks>), UiNavigator.Push, DevConsoleScreen.OnEnter, DevLogScreen.OnEnter.