validate derives the multiplexer backend from two independent selections, so one report can name two different backends.
What happens
platform_preflight (src/bmad_loop/runsetup.py:478) resolves the backend twice:
runsetup.py:503 — get_multiplexer(), an @lru_cache(maxsize=1) over _select() (src/bmad_loop/adapters/multiplexer.py:618). This is the authoritative pick: it is the instance the run will actually drive. It reports as mux.backend.
runsetup.py:534 — detect_multiplexers(), which calls _select() a second time (multiplexer.py:655) and re-probes available() on every candidate. Its selected row feeds mux.selection (runsetup.py:576) and the host.win32-on-wsl-path warning's "<X> was selected" clause (runsetup.py:637).
_select steps 3–4 gate on _usable() → available(), which shells out (tmux -V; psmux additionally needs pwsh). If that answer differs between the two calls — a transient fork failure, an antivirus lock, a binary appearing or disappearing — the two findings disagree inside one report, and host.win32-on-wsl-path asserts that a backend "was selected" which the process will not use. That check exists specifically to stop confidently-wrong output, so it is the worst place for it.
Why it is newly reachable
Pre-#485 the gate was if chosen and chosen.reason in ("env", "policy") — precisely the two _select() paths that return before any available() probe, so the second selection was deterministic and could not diverge. #485 un-gated it to if chosen:, which is correct for its purpose (platform-default is the reason #332 needed named) but exposes the three reasons that do depend on available().
Severity
Low: cosmetic in validate's own output, no in-repo consumer branches on the detail (documents.py only documents it; checks.py only registers the id), and it cannot change which backend a run drives.
Why the obvious fix is wrong
"Carry label forward instead of re-deriving from chosen" does not work: mux.backend reports a class name (type(backend).__name__ → TmuxMultiplexer, runsetup.py:504) while mux.selection reports a registry name (chosen.name → tmux). They are different namespaces, so there is nothing to compare or substitute directly, and swapping one for the other would change the message wording and the detail value.
A correct fix wants a cached accessor exposing the full (instance, name, reason) triple that _select() already returns, so mux.backend and mux.selection share one authoritative selection. get_multiplexer() currently discards two thirds of it (return _select()[0]). That is a change to the multiplexer seam, which is why it was kept out of #485 — that PR's safety argument is that git diff over adapters/ is empty.
Found by Codex review on #485 (runsetup.py:579); triaged there and deliberately deferred rather than grown into that PR.
validatederives the multiplexer backend from two independent selections, so one report can name two different backends.What happens
platform_preflight(src/bmad_loop/runsetup.py:478) resolves the backend twice:runsetup.py:503—get_multiplexer(), an@lru_cache(maxsize=1)over_select()(src/bmad_loop/adapters/multiplexer.py:618). This is the authoritative pick: it is the instance the run will actually drive. It reports asmux.backend.runsetup.py:534—detect_multiplexers(), which calls_select()a second time (multiplexer.py:655) and re-probesavailable()on every candidate. Itsselectedrow feedsmux.selection(runsetup.py:576) and thehost.win32-on-wsl-pathwarning's "<X>was selected" clause (runsetup.py:637)._selectsteps 3–4 gate on_usable()→available(), which shells out (tmux -V; psmux additionally needspwsh). If that answer differs between the two calls — a transient fork failure, an antivirus lock, a binary appearing or disappearing — the two findings disagree inside one report, andhost.win32-on-wsl-pathasserts that a backend "was selected" which the process will not use. That check exists specifically to stop confidently-wrong output, so it is the worst place for it.Why it is newly reachable
Pre-#485 the gate was
if chosen and chosen.reason in ("env", "policy")— precisely the two_select()paths that return before anyavailable()probe, so the second selection was deterministic and could not diverge. #485 un-gated it toif chosen:, which is correct for its purpose (platform-defaultis the reason #332 needed named) but exposes the three reasons that do depend onavailable().Severity
Low: cosmetic in
validate's own output, no in-repo consumer branches on the detail (documents.pyonly documents it;checks.pyonly registers the id), and it cannot change which backend a run drives.Why the obvious fix is wrong
"Carry
labelforward instead of re-deriving fromchosen" does not work:mux.backendreports a class name (type(backend).__name__→TmuxMultiplexer,runsetup.py:504) whilemux.selectionreports a registry name (chosen.name→tmux). They are different namespaces, so there is nothing to compare or substitute directly, and swapping one for the other would change the message wording and thedetailvalue.A correct fix wants a cached accessor exposing the full
(instance, name, reason)triple that_select()already returns, somux.backendandmux.selectionshare one authoritative selection.get_multiplexer()currently discards two thirds of it (return _select()[0]). That is a change to the multiplexer seam, which is why it was kept out of #485 — that PR's safety argument is thatgit diffoveradapters/is empty.Found by Codex review on #485 (
runsetup.py:579); triaged there and deliberately deferred rather than grown into that PR.