Skip to content

psmux backend: _transportable admits -X=value option values, which psmux 3.4.0 silently stores as -X value (regression from the #900 retirement) #905

Closed

Description

@dracic

Summary

#900 raised the psmux floor to 3.4.0 and dropped _transportable's refusal of every --leading @ option value, because 3.4.0 stores -u, -g, -x and similar values verbatim (psmux/psmux#583). Three shapes still do not survive psmux 3.4.0's client argv normalizers and set-option parser:

  • -X=value: -X=value with a nonempty suffix and an ASCII letter other than lowercase t is rewritten before option parsing; measured examples -x=foo and -a=b c read back as -x foo and -a b c at rc 0 with no warning on both bmad-loop paths (3/3 fresh sessions). The alphabetic family is source-derived from psmux v3.4.0 src/cli.rs:386; lowercase t takes the failure path below. Empty-suffix controls -x= and -a= round-trip verbatim. bmad-loop's set_session_option and the per-window channel (@<option>__blw@<N> keys) pass these values straight through, so the value reads back different from what was written, silently. Silently diverging values are exactly what _transportable exists to prevent.
  • -t… (-t, -tx, -t=foo, -target, …) and --: psmux refuses them with rc 1. On the session path bmad-loop raises TmuxError partway through the write. On the window path it warns. Neither frees the key the way the gate's own refusal does: a rewrite of an existing value to one of these shapes leaves the old value in place (observed on both paths).

tmux 3.4 stores all three shapes verbatim. The psmux side is reported in the psmux report (psmux/psmux#795). bmad-loop needs a workaround until a psmux release carries the fix.

Impact is source-derived: no value bmad-loop writes today has these shapes (hex project tags from runs.project_tag, =session:%N return targets and detach all round-trip). This reproduction demonstrates a latent backend contract defect; it does not demonstrate a real current run becoming unprunable. That consequence is conditional on a caller writing such a value, for example a tag that reads back different and leaves a window unprunable. It is still a hole the gate was built to close, and #900 reopened it.

Environment

psmux 3.4.0 (20aa1d7), Windows 11 10.0.26200, PowerShell 7.6.6 (the launching shell; SHELL removed), bmad-loop main 71ee261.

Steps to reproduce

Launched from PowerShell 7 in a bmad-loop checkout (uv sync done). Zero LLM tokens. Each value is written over a preexisting seed, through both bmad-loop paths: set_session_option and the per-window channel (set_window_option / show_window_option).

Remove-Item Env:SHELL -ErrorAction SilentlyContinue
$env:PYTHONUTF8 = '1'; $env:PSMUX_NO_WARM = '1'
$env:PSMUX_DATA_DIR = "$env:TEMP\bl-dash\psmux-data"
New-Item -ItemType Directory -Force $env:PSMUX_DATA_DIR | Out-Null
psmux new-session -d -s bl-dash
uv run python -c @'
import contextlib, io
from bmad_loop.adapters.psmux_backend import PsmuxMultiplexer
m = PsmuxMultiplexer()
S, W = "bl-dash", "bl-dash:@1"
paths = {
    "session": (lambda v: m.set_session_option(S, "@k", v),
                lambda: m._run(["show-options", "-qv", "-t", S, "@k"], check=False).stdout.strip()),
    "window": (lambda v: m.set_window_option(W, "@k", v), lambda: m.show_window_option(W, "@k")),
}
for v in ["plain", "-u", "-1=x", "--x=y", "-x=", "-x=foo", "-a=b c", "-t", "-tx", "-t=foo", "-target", "--"]:
    for path, (put, get) in paths.items():
        put("seed")
        err, note = io.StringIO(), ""
        with contextlib.redirect_stderr(err):
            try:
                put(v)
            except Exception as exc:
                note = f"{type(exc).__name__}: {exc}"
        note = note or err.getvalue().strip()
        got = get()
        verdict = "ok" if got == v else ("STALE" if got == "seed" else "DIFFERENT")
        print(f"{path:7} {v!r:9} -> {got!r:9} {verdict:9} {note}")
'@
if ($env:PSMUX_DATA_DIR -like "$env:TEMP\bl-dash\*") { psmux kill-server }
Remove-Item Env:PSMUX_DATA_DIR

Expected

Each value reads back exactly as written, or bmad-loop refuses it with its usual warning and frees the key.

Actual

session 'plain'   -> 'plain'   ok
window  'plain'   -> 'plain'   ok
session '-u'      -> '-u'      ok
window  '-u'      -> '-u'      ok
session '-1=x'    -> '-1=x'    ok
window  '-1=x'    -> '-1=x'    ok
session '--x=y'   -> '--x=y'   ok
window  '--x=y'   -> '--x=y'   ok
session '-x='     -> '-x='     ok
window  '-x='     -> '-x='     ok
session '-x=foo'  -> '-x foo'  DIFFERENT
window  '-x=foo'  -> '-x foo'  DIFFERENT
session '-a=b c'  -> '-a b c'  DIFFERENT
window  '-a=b c'  -> '-a b c'  DIFFERENT
session '-t'      -> 'seed'    STALE     TmuxError: psmux set-option -t failed: psmux: set-option: target required after -t
window  '-t'      -> 'seed'    STALE     warning: set-option @k__blw@1 failed: psmux: set-option: target required after -t
session '-tx'     -> 'seed'    STALE     TmuxError: psmux set-option -t failed: psmux: set-option: empty value for '@k'
window  '-tx'     -> 'seed'    STALE     warning: set-option @k__blw@1 failed: psmux: set-option: empty value for '@k__blw@1'
session '-t=foo'  -> 'seed'    STALE     TmuxError: psmux set-option -t failed: psmux: set-option: empty value for '@k'
window  '-t=foo'  -> 'seed'    STALE     warning: set-option @k__blw@1 failed: psmux: set-option: empty value for '@k__blw@1'
session '-target' -> 'seed'    STALE     TmuxError: psmux set-option -t failed: psmux: set-option: empty value for '@k'
window  '-target' -> 'seed'    STALE     warning: set-option @k__blw@1 failed: psmux: set-option: empty value for '@k__blw@1'
session '--'      -> 'seed'    STALE     TmuxError: psmux set-option -t failed: psmux: set-option: empty value for '@k'
window  '--'      -> 'seed'    STALE     warning: set-option @k__blw@1 failed: psmux: set-option: empty value for '@k__blw@1'

The -X=value rows: no warning and no exception, and the write reports success. Bare -t raises TmuxError: psmux set-option -t failed: psmux: set-option: target required after -t. The measured -tx, -t=foo, -target and -- values raise empty value for '@k' on the session path. The window path warns instead. Both leave a preexisting value unchanged. Independently reproduced in 3 of 3 fresh sessions on both paths. For contrast, a value the gate itself refuses (for example one containing ") warns "the key is freed" and reads back as unset on both paths.

Source

src/bmad_loop/adapters/psmux_backend.py at 71ee261. _transportable (line 932) has no dash rule since #900; its checks are only these:

# src/bmad_loop/adapters/psmux_backend.py:950
if not value:
    return False
if '"' in value or value != value.strip():
    return False
return value.splitlines() == [value]

set_session_option gates on it at line 977 and frees refused values at :984; an admitted value goes straight to the raw write:

# src/bmad_loop/adapters/psmux_backend.py:984
            self._write_scoped(["set-option", "-u", "-t", name, option], option)
            return
        super().set_session_option(name, option, value)

The window channel gates on it at line 1019, frees refused keys at :1028 and writes admitted values at :1030 through _write_scoped (:989-1003), which warns on a failed write instead of raising.

Suggested fix

Proposed, not validated: have _transportable refuse the shapes psmux 3.4.0 cannot carry, so they take the existing visible path: a warning, and the key freed. Concretely:

  • a value matching ^-[A-Za-z]=. (at least one character after the equals sign, in addition to the gate's existing whitespace/line-break exclusions): the silent one;
  • a value starting with -t, or equal to -- (refused by psmux today, but refusing them up front keeps the session path from raising partway through and the window path from keeping a stale value).

Preserve the existing gate's other restrictions and keep the measured safe controls -u, -1=x, --x=y, -x= and -a= admitted. The existing single-value live row does not establish all other dash-leading strings, and not every ASCII-letter variant was live-tested. Add a live-gate tripwire next to test_adopted_a_dash_leading_option_value_is_stored: a test_premise_* row that turns red when a psmux release stores -x=foo verbatim, so the narrowed refusal can retire behind a floor bump, the same way #900 retired the old one. One tripwire for -x=foo alone does not prove the -t…/-- defects are repaired; add independent retirement checks for those refusals.

Refs #900

Activity

  1. added a commit that references this issue on Oct 15, 2026
    5a482b1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions