Корень ошибки из диагностики 31.10.2026 (0.8.51, Hysteria2 без UDP из МГТС) —
в механизме, а не в одном месте:
1. «Какой сервер» имел три протокола: родной пул писал selected_node_id до
горячего свитча, Hysteria — до readiness нового sidecar, Amnezia держала
выбор в pending. После отказа прежний runtime работал, а выбранным и
сохранённым оставался отказавший сервер.
2. Каждый шаг перехода сам писал ошибку в статус — даже вытесненный новым
кликом («Не удалось запустить sing-box proxy runtime»), а поздний общий
текст затирал точную причину; фаза «error» ставилась при живом соединении.
Новые модули:
- application/server_selection.py — единственная транзакция выбора для всех
протоколов и видов перехода: при живом подключении новый сервер — только
цель (pending), выбранным становится в commit() готового runtime; неудача
её сбрасывает (откатывать нечего), без подключения выбор фиксируется сразу.
- application/transition_outcome.py — шаги сообщают причину
(_fail_transition), сильнейшая побеждает (SECURITY > CAUSE > CORE > STEP),
публикует один раз _on_transition_runner_finished; вытесненный переход
молчит; фаза выводится из того, что работает («Подключено» + «Соединение
осталось на «…»»).
- diagnostics/transport_failure.py (из предыдущего коммита) даёт причину CAUSE.
- signature_service считает желаемый runtime по цели, а не по выбранному.
E2E tests/test_app_e2e_hysteria_udp_switch.py на win10: настоящие sing-box и
hysteria, рабочий сервер и UDP-«чёрная дыра». v0.8.51: 3/3 FAIL (выбран
отказавший сервер; ложная ошибка sing-box; нет причины). Новый код: 3/3 OK.
Полный набор Windows: 1831 OK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>