You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
retire _transportable's refusal of -X=value, -t... and -- once a psmux release carries the psmux/psmux#795 fix #914
psmux fixed psmux/psmux#795 on master in 20ef00fb. Until then, its client argv normalizers rewrote option values shaped like flags: -x=foo was stored as -x foo, and -t… and a bare -- were refused. bmad-loop works around that in #908 (#905): _transportable refuses those three shapes, so they take the visible path (a warning, and the key freed) instead of being stored corrupted. As of 2026-10-14 no release carries the fix. The latest tag is v3.4.0, and git tag --contains 20ef00fb is empty.
What retires: the clause in PsmuxMultiplexer._transportable (src/bmad_loop/adapters/psmux_backend.py):
Also retire the sentence in docs/multiplexer-backends.md ("Refused option values") that names these three shapes.
What stays: every other _transportable rule (empty, a double quote, a line break, edge whitespace). Each one has its own reason in the docstring, and none of them depends on psmux/psmux#795.
The trigger
tests/test_psmux_live.py::test_premise_an_argv_flag_shaped_option_value_is_not_stored pins the 3.4.0 behaviour for each shape: -x=foo reads back -x foo at rc 0, and -t… and -- fail. It turns red on a psmux build that stores them verbatim. The weekly psmux-canary workflow runs the live gate against the latest psmux release, so that is where the red shows first.
Move _LAST_UNSUPPORTED to the last release without 20ef00fb (the gate is exclusive), and move the CI gate's pin (ci.yml, version:) to the first release that has it.
Delete the clause and its doc sentence.
Turn the premise row into an ordinary-direction test_adopted_* row: the three shapes are stored verbatim. Show that it fails against a fake of the 3.4.0 behaviour.
Add a CHANGELOG entry: an upgrade item for the floor, plus Removed for the refusal.
On any admitted build below the fix, removing the clause brings back silent corruption of a -X=value option value. A tag that reads back differently makes a window silently unprunable.
Checked: bmad-loop is not exposed to the rest of psmux/psmux#795
While fixing it, the maintainer found the same corruption in rename-session, set-buffer, new-window -n and display -p. bmad-loop never passes those commands a value starting with -:
session names are bmad-loop-<run id> and bmad-loop-ctl;
window names are run, sweep and control-window names that start with a letter;
it does not use set-buffer or rename-session;
its display-message -p calls pass only format strings that start with #{.
So _transportable remains the only workaround to retire.
Retirement evidence from psmux master f720eddc: the clause is unnecessary there for every tested value, and still needed on 3.4.0
Measured on psmux master f720eddc (built from source, unreleased; as checked on 2026-10-14 the latest release is still v3.4.0, released 2026-10-13, and no tag contains 20ef00fb), with bmad-loop at 45bac91c, Windows 11 build 26200, launched from PowerShell 7.6.6 with SHELL removed and PSMUX_NO_WARM=1. This adds the bmad-loop-side half of the retirement evidence. With only the dash-shaped refusal condition removed in a probe-local subclass, master stores every value in the table byte-exact through bmad-loop's session and window writers, and all four readers return it unchanged (3/3 independent runs per binary, plus further runs of the repro below). These are representative values for the three refused families, not an exhaustive proof over every matching string.
The trigger fires on master, as designed. In a full-file run of tests/test_psmux_live.py on the master binary, and in three -k premise runs, all three rows of test_premise_an_argv_flag_shaped_option_value_is_not_stored fail the same way every run, reading back (0, '-x=foo'), (0, '-tx') and (0, '--'); the only other failure in the full-file run was the expected test_premise_window_scoped_option_write_lands_at_session_scope flip. On 3.4.0 the premise tests pass.
Through bmad-loop's code, clause kept and clause removed. A probe-local subclass of PsmuxMultiplexer binds _BINARY to the binary's full path. With the clause kept (production) it is the positive control; then _transportable is replaced by the same function minus its --leading line. For each value it seeds "seed" on both channels, writes the value through set_session_option(S, "@tag", v) and set_window_option(S:@N, "@wtag", v), then reads it back through session_options("@tag"), show_window_option, _scoped_options and the @wtag column of list_windows:
value
master f720eddc, clause removed (all four readers)
3.4.0, clause removed
both binaries, clause kept
plain
exact
exact
exact
-x=foo
-x=foo exact
-x foo (rc 0, silently corrupted)
refused with a warning, reads ''
-X=a b
exact
-X a b
refused
-tfoo, -t=foo, --
exact
write fails rc 1 (set-option: empty value for '@tag'); set_session_option raises TmuxError, the window channel warns; seed stays
For the tested values, this clause is unnecessary on master; on 3.4.0 it still prevents the corruption or write failures in the first six value rows. Retirement remains gated on raising the supported release floor. One detail the table adds to the issue body: on 3.4.0 an unrefused -t… or -- value makes set_session_optionraise, not merely leave the seed in place, because BaseTmuxBackend.set_session_option uses the strict _tmux path. That is another reason the clause cannot go before the floor moves past 3.4.0.
Source-derived: at bmad-loop 45bac91, src/bmad_loop/adapters/psmux_backend.py:975 is the dash-shaped refusal; :1009 delegates accepted session values to BaseTmuxBackend.set_session_option, whose src/bmad_loop/adapters/tmux_base.py:386-389 uses strict _tmux. Base _run raises on a non-zero rc at tmux_base.py:157-158. Window writes instead use _write_scoped at psmux_backend.py:1011-1026, with check=False and a warning on a non-zero rc. In psmux f720eddc, src/cli.rs:718-722 stops normalization at the first operand, and :1945-1954 exempts a lone trailing -- or -t after the option name from the extension parser. Commit 20ef00fb introduces these changes.
The October hold on send-keys -l stays irrelevant. Source-derived at 45bac91: the in-tree generic adapter's four nudge call sites (generic.py:1610, :1780, :1990, :3366) send NUDGE_TEXT, STALL_NUDGE_TEXT, BUDGET_NUDGE_TEXT or a formatted CONTRACT_NUDGE_TEXT, each with a fixed You-prefixed start (generic.py:222-262). No examined in-tree nudge caller supplies a dash-leading operand to send-keys -l. opencode_http.py uses the same constants, but its send_text goes over HTTP, not psmux.
Repro (run from PowerShell 7 with SHELL removed, in a bmad-loop checkout at 45bac91c after uv sync --all-extras; -Bin is the psmux to test, by full path; it uses a throwaway registry under %TEMP% and removes it):
repro-914.ps1
# Run from PowerShell 7 (pwsh) in a bmad-loop checkout at 45bac91c after `uv sync --all-extras`.# pwsh -NoProfile -File repro-914.ps1 -Bin C:\path\to\psmux.exe # the master build, then the installed 3.4.0 as the controlparam([Parameter(Mandatory)][string]$Bin)
$ErrorActionPreference='Stop'Remove-Item Env:SHELL -ErrorAction SilentlyContinue
$env:PSMUX_NO_WARM='1'; $env:PYTHONUTF8='1'$Data=Join-Path$env:TEMP"psmux-914-probe-$PID"; New-Item-ItemType Directory -Force $Data|Out-Null$Py=Join-Path$env:TEMP"probe914-$PID.py"@'import contextlib, io, sysfrom pathlib import Pathfrom bmad_loop.adapters.psmux_backend import PsmuxMultiplexerbinary, data = sys.argv[1], sys.argv[2]class M(PsmuxMultiplexer): _BINARY = binarydef relaxed(v): # _transportable minus its "-"-leading line return bool(v) and '"' not in v and v == v.strip() and v.splitlines() == [v]m = M(registry_root=data)print(" / ".join(m._run(["-V"]).stdout.split()))S = "probe-914"m.new_session(S, Path(data))try: w = m.new_window(S, "w1", Path(data), {}, "pwsh -NoProfile -Command 'Start-Sleep 300'") key = m._scoped_option_key("@wtag", w.rsplit("@", 1)[1]) def reads(): # all four readers opts = m._scoped_options(S) or {} return [m.session_options("@tag").get(S), m.show_window_option(w, "@wtag"), opts.get(key), dict(m.list_windows(S, ["window_id", "@wtag"])).get(w)] for phase, values in [("clause kept", ["plain", "-x=foo", "-tfoo", "--", "-t"]), ("clause removed", ["plain", "-x=foo", "-X=a b", "-tfoo", "-t=foo", "--", "-t", "-x=", "-u", "--x=y", "-1=x", "-w", "-g"])]: if phase == "clause removed": M._transportable = staticmethod(relaxed) print(f"== {phase}") for v in values: m.set_session_option(S, "@tag", "seed"); m.set_window_option(w, "@wtag", "seed") err, raised = io.StringIO(), "" with contextlib.redirect_stderr(err): try: m.set_session_option(S, "@tag", v) except Exception as e: raised = f" raised {type(e).__name__}: {e}" m.set_window_option(w, "@wtag", v) warned = " window warned" if err.getvalue() else "" print(f"{v!r:10} -> {reads()}{raised}{warned}")finally: m.kill_session(S)'@|Set-Content$Pytry {
uv run python $Py$Bin$Data
} finally {
$env:PSMUX_DATA_DIR=$Dataif ($env:PSMUX_DATA_DIR-eq$Data) { &$Bin kill-server 2>$null|Out-Null }
Remove-Item Env:PSMUX_DATA_DIR
Start-Sleep1Remove-Item-Recurse -Force $Data,$Py-ErrorAction SilentlyContinue
}
Output on master (the clause-kept block is the same on 3.4.0):
Activity
dracic commentedon Oct 14, 2026
Retirement evidence from psmux master f720eddc: the clause is unnecessary there for every tested value, and still needed on 3.4.0
Measured on psmux master
f720eddc(built from source, unreleased; as checked on 2026-10-14 the latest release is stillv3.4.0, released 2026-10-13, and no tag contains20ef00fb), with bmad-loop at45bac91c, Windows 11 build 26200, launched from PowerShell 7.6.6 withSHELLremoved andPSMUX_NO_WARM=1. This adds the bmad-loop-side half of the retirement evidence. With only the dash-shaped refusal condition removed in a probe-local subclass, master stores every value in the table byte-exact through bmad-loop's session and window writers, and all four readers return it unchanged (3/3 independent runs per binary, plus further runs of the repro below). These are representative values for the three refused families, not an exhaustive proof over every matching string.The trigger fires on master, as designed. In a full-file run of
tests/test_psmux_live.pyon the master binary, and in three-k premiseruns, all three rows oftest_premise_an_argv_flag_shaped_option_value_is_not_storedfail the same way every run, reading back(0, '-x=foo'),(0, '-tx')and(0, '--'); the only other failure in the full-file run was the expectedtest_premise_window_scoped_option_write_lands_at_session_scopeflip. On 3.4.0 the premise tests pass.Through bmad-loop's code, clause kept and clause removed. A probe-local subclass of
PsmuxMultiplexerbinds_BINARYto the binary's full path. With the clause kept (production) it is the positive control; then_transportableis replaced by the same function minus its--leading line. For each value it seeds"seed"on both channels, writes the value throughset_session_option(S, "@tag", v)andset_window_option(S:@N, "@wtag", v), then reads it back throughsession_options("@tag"),show_window_option,_scoped_optionsand the@wtagcolumn oflist_windows:plain-x=foo-x=fooexact-x foo(rc 0, silently corrupted)''-X=a b-X a b-tfoo,-t=foo,--set-option: empty value for '@tag');set_session_optionraisesTmuxError, the window channel warns;seedstays''-tset-option: target required after -t; raises / warns;seedstays''-x=,-u,--x=y,-1=x,-w,-gFor the tested values, this clause is unnecessary on master; on 3.4.0 it still prevents the corruption or write failures in the first six value rows. Retirement remains gated on raising the supported release floor. One detail the table adds to the issue body: on 3.4.0 an unrefused
-t…or--value makesset_session_optionraise, not merely leave the seed in place, becauseBaseTmuxBackend.set_session_optionuses the strict_tmuxpath. That is another reason the clause cannot go before the floor moves past 3.4.0.Source-derived: at bmad-loop 45bac91,
src/bmad_loop/adapters/psmux_backend.py:975is the dash-shaped refusal;:1009delegates accepted session values toBaseTmuxBackend.set_session_option, whosesrc/bmad_loop/adapters/tmux_base.py:386-389uses strict_tmux. Base_runraises on a non-zero rc attmux_base.py:157-158. Window writes instead use_write_scopedatpsmux_backend.py:1011-1026, withcheck=Falseand a warning on a non-zero rc. In psmuxf720eddc,src/cli.rs:718-722stops normalization at the first operand, and:1945-1954exempts a lone trailing--or-tafter the option name from the extension parser. Commit20ef00fbintroduces these changes.The October hold on
send-keys -lstays irrelevant. Source-derived at 45bac91: the in-tree generic adapter's four nudge call sites (generic.py:1610,:1780,:1990,:3366) sendNUDGE_TEXT,STALL_NUDGE_TEXT,BUDGET_NUDGE_TEXTor a formattedCONTRACT_NUDGE_TEXT, each with a fixedYou-prefixed start (generic.py:222-262). No examined in-tree nudge caller supplies a dash-leading operand tosend-keys -l.opencode_http.pyuses the same constants, but itssend_textgoes over HTTP, not psmux.Repro (run from PowerShell 7 with
SHELLremoved, in a bmad-loop checkout at45bac91cafteruv sync --all-extras;-Binis the psmux to test, by full path; it uses a throwaway registry under%TEMP%and removes it):repro-914.ps1
Output on master (the clause-kept block is the same on 3.4.0):
Output on 3.4.0, clause removed (paths shortened):
Nothing here changes the plan in the body; it is still gated on a tagged psmux release that contains
20ef00fb.