Skip to content

Native-Windows psmux multiplexer backend: re-land as a thin leaf over the dialect seams #58

Description

@dracic

Context

PR #19 (feat/psmux-backend) was the exploratory implementation of a native-Windows terminal-multiplexer backend driving psmux's tmux.exe drop-in (ConPTY, no WSL). It proved the approach end-to-end — real dev sessions ran on native Windows — but it was closed rather than merged: it predates the bmad-loop rename and the adapter seams, and exercising it surfaced engine-level defects that had to land on main first. This issue tracks re-landing the backend as a fresh, thin leaf on top of today's seams.

What the test PR/branch surfaced

Preparation that already landed on main

Fresh verification on live psmux 3.3.6 (claude CLI 2.1.200, Windows 11 26200)

  • Quoted command strings do not survive the tmux→ConPTY→shell layers: psmux joins trailing argv and runs it through an outer shell, so embedded quoting is lost. Window scripts must travel as pwsh -NoProfile -EncodedCommand <base64 UTF-16LE> — no quotes to lose.
  • new-session -e VAR=val propagates to later windows; new-window -e is accepted but silently not applied — per-window env must ride an in-source $env:K='v' prelude.
  • PSMUX_ALLOW_NESTING=1 bypasses the nesting guard (the documented knob) — one added env var on the create call instead of stripping PSMUX_SESSION/PSMUX_ACTIVE/TMUX.
  • Windows inherit the full env of the process that cold-started the server (including any Claude-Code session vars) plus the injected agent-teams vars — so every launched window's source starts by clearing the teammate-mode vars.
  • Two previously-suspected external blockers did not reproduce: CLI startup inside a psmux window is fine, and session transcripts persist with TMUX set (print mode; one interactive spot-check remains).

Plan

Additive leaf src/bmad_loop/adapters/psmux_backend.py (~190 lines), no contract-method-body overrides beyond the documented divergences:

  1. _ENCODING = "utf-8" plus a small platform-neutral base addition: an _ERRORS class attr (default None = strict, POSIX byte-identical) forwarded to subprocess.run, so a stray ConPTY byte degrades one character instead of raising out of every read.
  2. Dialect hooks: _EXIT_CAPTURE = "$ec = $LASTEXITCODE", _ECHO = "Write-Host", _PARK = "Read-Host"; _join_argv& 'a' 'b' (single-quote escaped); _shell_wrappwsh -NoProfile -EncodedCommand; _parked_trailer re-expressed in pwsh (same tmux verbs); _window_launch → teammate-clear + validated env prelude + explicit call, wrapped via _shell_wrap.
  3. Divergent methods: new_session spawns with PSMUX_ALLOW_NESTING=1 layered on the parent env; kill_session uses plain -t name; pipe_pane attaches a best-effort pwsh Add-Content sink; available() requires psmux + tmux + pwsh on PATH.
  4. Registration: register_multiplexer("psmux", platform == "win32", …) — WSL reports linux and stays on tmux.
  5. Deterministic unit tests (mocked spawn seam, green on every OS: encoded-pwsh construction, nesting-guard env, plain-name kill, selection matrix) + a live smoke on a Windows host.
  6. Interactive-only follow-up: verify attach -t / switch-client -t targeting and the parked return-trailer on a real client (3.3.6 source says both work; needs a human at a terminal).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions