Windows support fixes (psmux-compatible)#207
Conversation
|
For context: this PR is independent of #206 (the multiplexer-abstraction / WezTerm-backend work).
The two paths are complementary, not competing:
A few of the smaller fixes in here (e.g. the Happy to split / rework as preferred. |
98a7c28 to
afcb563
Compare
|
@psmux — flagging for confirmation. Thanks again for offering help in #206. While porting CAO to run with psmux as a drop-in tmux replacement on Windows, we hit a handful of behavioural deltas vs upstream tmux. We've worked around all of them on the CAO side (every site tagged with `# psmux-workaround: …` in clients/tmux.py for grep-ability), but it would be ideal to land upstream fixes so the workarounds can be removed. Tested against psmux 3.3 on Windows 11 with libtmux 0.51.0 in the loop. Findings
AskCould you confirm any of these are accurate / scoped correctly? Findings #1–#4 in particular feel like one-or-two-line server-side fixes that would let libtmux drive psmux directly without our subprocess workaround layer. |
|
Will look into it and help you get it up and running in a short bit. Also, you don't need to workaround except for OS specific issues. I will help you undo the workarounds if it's because of Psmux (except specific OS related stuff) |
|
Hey @marcfargas, great work on this PR. I went through every single workaround you documented and tested them all thoroughly. Here is a complete status report with proof. All 6 Workarounds Tested and VerifiedI wrote three separate test suites to verify each finding:
Full cargo test suite: 5619 tests, 0 failures. Workaround StatusWA1:
|
| # | Issue | Status | Workaround needed? |
|---|---|---|---|
| 1 | list-sessions ignores -F |
Fixed | No |
| 2 | -F#{fmt} concatenated ignored |
Fixed | No |
| 3 | has-session -t =NAME |
Fixed | No |
| 4 | -e KEY=VAL not propagated |
Works | No |
| 5 | Named paste buffers | Fixed | No |
| 6 | paste-buffer -p bracketed |
Known limitation | Not a blocker |
Workarounds 1 through 5 are proven unnecessary in psmux 3.3.3. I am happy to help you strip them out if you want, or you can grep for # psmux-workaround: and remove them yourselves. Let me know what works best.
All test files are in the psmux repo under tests/ if you want to verify independently.
Update: All 6 Workarounds Eliminated + libtmux Native API WorksCommit: Installcargo install --git https://github.com/psmux/psmux --tag v3.3.0.4What's FixedEvery workaround in PR #207 is now provably unnecessary with psmux:
libtmux Native API Compatibilitylibtmux's
Test Evidence33 Python tests (all pass):
26 PowerShell E2E workaround elimination tests (all pass) 22 PowerShell E2E PR207 compat tests (all pass) 33 Rust unit tests (22 named buffers + 11 PR207 compat) (all pass) 1861 cargo tests (all pass) |
|
@marcfargas @haofeif Please try it and let me know how it goes |
|
Thanks @psmux — that's a comprehensive set of fixes. One snag on our end: we don't have a Rust toolchain on the test box, so The Could you dispatch the Release workflow to publish a binary? Two options that would work for us:
Once a binary is out, I'll upgrade locally, strip workarounds 1–5 from |
|
Okay I need to run comprehensive testing overall which will go for several hours. Please give me sometime for the release |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #207 +/- ##
=======================================
Coverage ? 10.81%
=======================================
Files ? 52
Lines ? 4326
Branches ? 0
=======================================
Hits ? 468
Misses ? 3858
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
7b10aa5 to
387c395
Compare
The custom CreateProcessW command-line builder wrapped each value in "..." and only replaced embedded quotes. For a value with spaces ending in a backslash (e.g. C:\Program Files\Foo Bar\plugins\), it produced "...plugins\" — the trailing \" is read by CommandLineToArgvW in the spawned server as an *escaped* quote, swallowing every subsequent argv entry into the value. Replace the naive quoter with escape_arg_msvcrt, which doubles backslash runs that immediately precede a " (literal or closing quote), per Microsoft's documented parsing rules. Tests: 10 new Rust units (incl. round-trip through real CommandLineToArgvW) and 17 E2E checks (Python subprocess, cmd /c, PowerShell native, control case, TUI verification). 1895 existing Rust tests pass. Reported by @marcfargas (downstream of awslabs/cli-agent-orchestrator#207)
|
Quick upstream status update on the remaining psmux workarounds:
I also updated the PR body to reflect this, but adding this comment so subscribers get notified. |
psmux runs on Windows under PowerShell; several modules assumed a
POSIX environment and failed silently or crashed on import.
- api/server.py: defer Unix-only signal/resource imports behind a
platform check so cao-server can start on Windows at all.
- clients/tmux: replace startswith("/") path test with os.path.isabs()
so Windows absolute paths (e.g. C:\...) are recognised correctly.
- providers/copilot_cli: guard Path.home() against RuntimeError when
USERPROFILE/HOME is cleared by the test harness.
- utils/logging: open FileHandler with delay=True to avoid Windows
file-lock errors during process cleanup.
- utils (skill_injection, agent_profiles, skills): use os.path.join
and pathlib throughout instead of forward-slash string concatenation.
- tests: skip POSIX-permission (chmod 000) and symlink tests on Windows
where those semantics are not available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…3.3.4 libtmux used the Python tmux bindings which assumed a native tmux socket and POSIX session management. psmux (the Windows tmux shim) diverges in ways that libtmux cannot paper over: session creation, env propagation, and buffer commands all behave differently. Replace the entire libtmux session layer in clients/tmux.py with direct subprocess calls to the tmux/psmux binary. This gives full control over the exact argv passed at each step and lets us handle platform differences explicitly. Session env is injected via repeated -e KEY=VAL flags; window names are forced with -n; paste-buffer IDs are UUID-named to avoid collisions. Providers are updated in parallel: commands are built as argv lists on Windows (instead of a shell string) so quoting is handled by the OS argument-passing layer rather than a POSIX shell that psmux doesn't provide. _pwsh_join is deduplicated into a single shared helper. The result: psmux 3.3.4 on Windows is fully supported with no platform-specific workarounds needed in the session layer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fixes Several edge cases discovered during real-world psmux testing on Windows after the subprocess client landed: - Env var filter: tmux and psmux reject env-key names that contain characters outside [A-Za-z0-9_]; filter them out before building the -e KEY=VAL flags to avoid a silent launch failure. - send-keys -l: psmux's paste-buffer implementation is broken on Windows; switch to send-keys -l (literal) which works correctly. - UTF-8 decoding: force encoding='utf-8' on all subprocess stdout/stderr reads so non-ASCII output doesn't crash on Windows code pages. - pwsh_join for claude_code provider: the provider was still building a bash-style shell string on Windows; route it through pwsh_join so quoting is correct for PowerShell. - Trailing backslash in env values: Windows paths often end with \; psmux treats a trailing backslash as a line-continuation and drops the value. Strip it before passing via -e. - pwsh_join & prefix: the joined command string must be prefixed with & so PowerShell treats it as an invocation expression, not a string. - Window rename: psmux ignores the -n flag on new-window when automatic-rename is on; explicitly call rename-window after creation. - PSReadLine history: CAO-injected pwsh commands were being saved to the user's command history; suppress with Set-PSReadLineOption before each injected command. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a callout under the tmux install step noting that Windows users should use psmux >= 3.3.4, as earlier versions lack the compatibility fixes this client depends on. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Record the agreed approach for verifying this CAO fork through a local uv tool install before deeper runtime checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Independent set of Windows-portability fixes that lets
cao-serverrun on Windowswhen psmux (a tmux-compatible Rust multiplexer,
https://github.com/psmux/psmux) is installed as
tmuxon PATH. Noarchitectural changes — vanilla CAO works on Windows with a drop-in tmux
replacement.
Thanks to @psmux for the very responsive turnaround on 3.3.4, which retired
five of the original CAO-side workarounds.
Why drop libtmux for session/window operations
clients/tmux.pypreviously usedlibtmuxforServer.new_session(),Session.from_session_id(), and friends. libtmux's session lookups depend on-Fformatting and=NAMEexact-match prefix being honoured by theunderlying binary, which earlier psmux releases didn't do consistently —
returning empty session lists and throwing
TmuxObjectDoesNotExist(manifesting as the misleading
zip() argument 2 is shorter).The fix replaces the libtmux session-management layer with direct
subprocess.run(["tmux", ...])calls using argument forms that both tmux andpsmux honour. libtmux is still imported for back-compat with mocks in
existing tests; only the lookup paths are bypassed. Linux/macOS behaviour
with real tmux is bit-for-bit unchanged.
What's in here (4 commits)
fix: Windows portability across api/utils/providers/tests— deferUnix-only imports in
api/main.py(fcntl/pty/termios);os.path.isabsfor Windows absolute paths;
Path.home()RuntimeErrorguard;FileHandler(delay=True)to avoid Windows file-locks during cleanup; URIhandling and skill-injection path fixes; POSIX-permission and symlink tests
skipped on Windows.
feat(clients/tmux): replace libtmux with subprocess client for psmux 3.3.4—the libtmux→subprocess refactor, env propagation via
-e KEY=VAL, argv-formcommand building in providers, deduplicated
pwsh_join.fix(clients/tmux,providers): Windows-specific pwsh and psmux quoting fixes—POSIX-name env filter,
send-keys -lfallback for the broken paste-buffer,UTF-8 subprocess decoding,
pwsh_join&-prefix and trailing-\strip,forced window rename, PSReadLine history suppression so CAO-injected commands
don't pollute the user's shell history.
docs(readme): require psmux >= 3.3.4 on Windows.psmux compatibility workarounds (CAO-side, pending upstream fixes)
Every site is grep-able as
# psmux-workaround: ...inclients/tmux.py.Three remain after psmux 3.3.4; each is linked to an open upstream issue:
paste-buffer(send_keys,send_keys_via_paste)send-keys -l(literal) on Windowsnew-session -e KEY=VALwith trailing-\value-eflags after a value ending in\followed by spacesnew-session -n NAME/new-window -n NAMEautomatic-renameand re-assert the name post-creationEach comment in the source links the issue so the workaround is removable
once the fix lands and a binary is published.
Test results
origin/mainbaseline.skipped / 78 platform-deselected.
Scope
Intentionally narrow. No multiplexer abstraction, no new modules, no new
dependencies. (The broader abstraction-layer approach in #206 was closed in
favour of this PR.) Each commit is self-contained and reverts cleanly. The
smaller portability fixes (
os.path.isabs,Path.home()guard,FileHandler(delay=True)) apply regardless of which multiplexer ships withCAO.
Test plan
cao launch --provider claude_code --agent-profile <profile>succeeds, agent getsCAO_TERMINAL_IDand anyCLAUDE_CODE_USE_*env vars🤖 Generated with Claude Code