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
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
Engine died on uncaught adapter exceptions — a list-windows timeout leaked subprocess.TimeoutExpired out of the polling loop and killed the engine mid-run. Fixed on main: crash recording + multiplexer seam honesty (0.7.9/0.7.10).
psmux behavioral divergences (verified against the 3.3.6 source and live probes, not assumed from docs): cp1252 garbles -F output; no /bin/sh for the parked-window recipe; the nesting guard silently no-ops new-session from inside a pane (exit 0, nothing created); kill-session -t =name is silently ignored (plain name works); agent-teams env vars are injected at the server level; no native cat for pipe-pane.
docs/adapter-authoring-guide.md and docs/porting-to-a-new-os.md.
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:
_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.
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.
Registration: register_multiplexer("psmux", platform == "win32", …) — WSL reports linux and stays on tmux.
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.
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).
Context
PR #19 (
feat/psmux-backend) was the exploratory implementation of a native-Windows terminal-multiplexer backend driving psmux'stmux.exedrop-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 thebmad-looprename and the adapter seams, and exercising it surfaced engine-level defects that had to land onmainfirst. This issue tracks re-landing the backend as a fresh, thin leaf on top of today's seams.What the test PR/branch surfaced
list-windowstimeout leakedsubprocess.TimeoutExpiredout of the polling loop and killed the engine mid-run. Fixed on main: crash recording + multiplexer seam honesty (0.7.9/0.7.10).git reset --harderased commits the attempt itself made. Fixed on main: fix(engine): preserve an attempt's commits before auto-rollback hard … #29 (preserve attempt commits, follow-ups Cap the lifecycle of attempt-preserve/* recovery refs (follow-up to #29) #32/Cap the lifecycle of refs/attempt-preserve-dirty/* worktree-snapshot refs (follow-up to #32) #49 → PRs feat(scm): bounded retention for attempt-preserve recovery refs #50/feat(scm): bounded retention for attempt-preserve-dirty worktree-snapshot refs #54), fix(engine): bound workflow-session stall nudges + spell out the completion contract #44 (completion contract). Residual completion/durability reports tracked in [BUG] result.json written and complete, but copilot session never signals Stop — hangs to session_timeout, discards work on rollback #34/[BUG] Dev session reported completed from the idle-poll shortcut while the agent window is still alive #48/[BUG] find_result_artifact / parse_auto_run_result treat a fence-quoted heading as a real terminal section #52/[BUG] Completed dev session not persisted before post_session hooks/verification (session-end durability gap) #57.os.kill(pid, 0)isCTRL_C_EVENTon win32 (phantomKeyboardInterrupts), PID reuse, unreadable-but-existing PIDs. Fixed on main:WindowsProcessHost, persisted PID identity, identity-aware liveness (engine.pid: verify process start-time identity to defend against PID reuse #12, fix(process): identity-aware liveness and win32 console-ctrl safety #30, win32 liveness: bias identity read to alive/unknown on an unreadable-but-existing pid (ACCESS_DENIED) — follow-up #2 to #30 #33, CLI resolve/delete/archive: surface unknown liveness (win32) — TUI/CLI parity follow-up to #36 #38).-Foutput; no/bin/shfor the parked-window recipe; the nesting guard silently no-opsnew-sessionfrom inside a pane (exit 0, nothing created);kill-session -t =nameis silently ignored (plain name works); agent-teams env vars are injected at the server level; no nativecatforpipe-pane.Preparation that already landed on main
_runseam: let subclasses set output encoding and per-call env without reimplementing the spawn primitive #40/refactor(adapters): let tmux _run carry output encoding and per-call env #41 —_runspawn seam carries output decoding (_ENCODINGclass attr) and a per-callenv; no_runoverride needed._shell_wrap,_join_argv,_source_prefix,_parked_trailer,_window_launch+ the_EXIT_CAPTURE/_ECHO/_PARKfragments), designed so a non-POSIX leaf overrides string fragments, never a contract method body.docs/adapter-authoring-guide.mdanddocs/porting-to-a-new-os.md.Fresh verification on live psmux 3.3.6 (claude CLI 2.1.200, Windows 11 26200)
pwsh -NoProfile -EncodedCommand <base64 UTF-16LE>— no quotes to lose.new-session -e VAR=valpropagates to later windows;new-window -eis accepted but silently not applied — per-window env must ride an in-source$env:K='v'prelude.PSMUX_ALLOW_NESTING=1bypasses the nesting guard (the documented knob) — one added env var on the create call instead of strippingPSMUX_SESSION/PSMUX_ACTIVE/TMUX.TMUXset (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:_ENCODING = "utf-8"plus a small platform-neutral base addition: an_ERRORSclass attr (defaultNone= strict, POSIX byte-identical) forwarded tosubprocess.run, so a stray ConPTY byte degrades one character instead of raising out of every read._EXIT_CAPTURE = "$ec = $LASTEXITCODE",_ECHO = "Write-Host",_PARK = "Read-Host";_join_argv→& 'a' 'b'(single-quote escaped);_shell_wrap→pwsh -NoProfile -EncodedCommand;_parked_trailerre-expressed in pwsh (same tmux verbs);_window_launch→ teammate-clear + validated env prelude + explicit call, wrapped via_shell_wrap.new_sessionspawns withPSMUX_ALLOW_NESTING=1layered on the parent env;kill_sessionuses plain-t name;pipe_paneattaches a best-effort pwshAdd-Contentsink;available()requirespsmux+tmux+pwshon PATH.register_multiplexer("psmux", platform == "win32", …)— WSL reportslinuxand stays on tmux.attach -t/switch-client -ttargeting and the parked return-trailer on a real client (3.3.6 source says both work; needs a human at a terminal).