Conversation
added 2 commits
September 8, 2026 13:16
…Windows sessions can launch) The native Windows agent installs, comes online and triages tickets but could not launch, capture, drive or serve ANY Claude session: every terminal call site in hub-agent.py shelled tmux/ttyd unconditionally and never invoked the pty-host XERK-668 built, so each died with `WinError 2`. This wires the manager side of that seam (ADR D5) — the piece XERK-668 shipped the protocol for but left unbuilt. On Windows, IS_WINDOWS dispatches every tmux/ttyd site to the per-session ConPTY pty-host, which replaces BOTH tmux and ttyd in one detached process: - _spawn_in_tmux + _launch_ttyd collapse into _spawn_pty_host: it spawns the pty-host (node-pty) DETACHED, serving the terminal on the session's stable ttydPort (the hub proxies /term unchanged) and running claude. _launch_tmux translates its POSIX shell command into argv + an env dict (node-pty spawns claude directly): the `VAR=x` env prefix -> env entries; `set -a; . <file>` failover source -> the file read and merged into the env dict (gateway key stays off the command line); flags -> portable argv, prompt positional after `--`. _windows_claude_launcher resolves claude.exe (argv) vs claude.cmd (cmd.exe /c) via PATHEXT. - _capture_pane / _type_into_pane / _tmux_alive / _pane_send_keys (interrupt, set_mode, set_model, answer_pane_prompt) drive the pty-host's JSON control websocket (inject/capture/alive/resize/kill) through a pure-Python RFC 6455 client (stdlib only, no ws dep). _kill_tmux tears the pty-host down; _kill_ttyd and _launch_ttyd no-op. Everything resolves from the pty-host's own state file, so resume-on-boot ADOPT is free. Linux path is untouched (every branch is behind IS_WINDOWS). Verified: full test_hub_agent suite green (2162); new TestWindowsTerminalBackend / TestWindowsTerminalBackendManager (26, mock IS_WINDOWS) pin framing, the shell->argv/env translation and every call-site dispatch. The pure-Python control client is proven cross-language against the real `ws` + tty-protocol.mjs control server (10/10). Real-Windows ConPTY (Session-0 service, job-object breakaway) stays host-verified separately — the pty-host itself is drive.mjs's 24/24 on real ConPTY, and agent/win/pty-control-drive.py is the host-proof manager-side drive. Deps (node-pty + ws) + lay-down are install.ps1's, already shipped.
…e env parse
Adversarial QA (PARTIAL) found two Windows-only defects in the seam:
- _spawn_pty_host now captures the detached Popen handle and terminate()s it on
the spawn-timeout branch before _pty_teardown + raise. A pty-host that started
but never published bound ports has no tmux backstop, so without this it was
ORPHANED — leaking the process and holding ttydPort so the next launch could
not rebind.
- _read_env_file un-quotes with shlex.split (the exact inverse of
write_local_model_env's shlex.quote, matching Linux `. <file>` sourcing) instead
of a naive strip('"').strip("'"), so a failover gateway value containing a
space or a quote round-trips into the pty-host env dict.
Also dropped _pty_inject's unreachable "bracketed-paste fallback" (the control
inject always succeeds when the channel is up; the branch only ran on a channel
failure, where the fallback failed too) and corrected its comment.
Re-QA (qa-delta) PASS: full suite green (2163), each fix's guard test proven
load-bearing by mutation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The native Windows agent installs, comes online, heartbeats and triages tickets — but could not launch, capture, drive, or serve any Claude session.
hub-agent.py's terminal layer shelledtmux/ttydunconditionally at every call site and never invoked the ConPTY pty-host XERK-668 built (agent/win/pty-host.mjs), so every Windows session died at launch withWinError 2: The system cannot find the file specified. This blocked the entire session/lifecycle/restart-survival surface of the Windows-agent epic (XERK-666).Bug verified still real on current
mainbefore changing anything:grep -n 'pty-host\|ConPTY' hub-agent.pyfound only comments; every terminal call site rantmux/ttydwith noIS_WINDOWSdispatch.This wires the manager side of the seam — the "D5 manager portability pass" the pty-host shipped the protocol for but left unbuilt.
What changed (all in
agent/hub-agent.py, entirely behindIS_WINDOWS)On Windows, one pty-host process per session replaces BOTH tmux and ttyd:
_spawn_in_tmux+_launch_ttydcollapse into_spawn_pty_host: spawns the pty-host (node-pty) detached, serving the terminal on the session's stablettydPort(the hub proxies/term/<id>unchanged) and running claude._launch_ttyd/_kill_ttydno-op;_kill_tmuxtears the pty-host down. Every kill/delete/restart path runs_kill_tmuxbefore_kill_ttyd, so nothing leaks._launch_tmuxtranslates its POSIX shell command into argv + an env dict (node-pty spawns claude directly, no shell): theVAR=xenv-assignment prefix → env entries; the failoverset -a; . <local-model.env>→_read_env_filemerged into the env dict (gateway key stays off the command line, as the source kept it); the claude flags → portable argv, prompt positional after--.--settings(guard + AskUserQuestion bridge) preserved._windows_claude_launcherresolvesclaude.exe(argv) vsclaude.cmd(cmd.exe /c claude) via PATHEXT._ws_control_rpc/_pty_control, stdlib only — nowsdependency on the Python side) drives the pty-host's JSON control websocket:inject/capture/alive/resize/kill, the 1:1 tmux-CLI replacements._capture_pane/_type_into_pane/_tmux_aliveand_pane_send_keys(interrupt / set_mode / set_model / answer_pane_prompt) all dispatch to it.{pid,ctrlPort,termPort}state file, so resume-on-boot ADOPT is free — a surviving detached pty-host is reached with no reattach code.The Linux path is untouched — every change short-circuits on
IS_WINDOWS. Deps (node-pty + ws) and file lay-down are alreadyinstall.ps1's job; the paths (<base>\win\pty-host.mjs,node) match.Rules updated:
.claude/rules/windows-agent.md(the now-wired manager seam) and.claude/rules/windows-terminal.md(D5 done).Verification
test_hub_agentsuite green: 2163 tests OK. NewTestWindowsTerminalBackend/TestWindowsTerminalBackendManager(mockIS_WINDOWS, 27 tests) pin the RFC 6455 framing, the shell→argv/env translation, credential-off-command-line, and every call-site dispatch.wslibrary + realagent/win/tty-protocol.mjscontrol server (alive/inject/capture/resize/kill, wrong-token/empty-token refusal, unknown-op, 2-byte and 8-byte length frames) — QA reproduced 12/12 and caught two client mutations (unmasked frame, wrong handshake GUID), proving masking and theSec-WebSocket-Acceptcheck are load-bearing.qa→qa-delta): first pass PARTIAL found two Windows-only defects — (1) the detached pty-host was orphaned on the spawn-timeout path (leaked process + heldttydPort); (2)_read_env_fileun-quoting wasn't the inverse ofwrite_local_model_env'sshlex.quote. Both fixed (capture the Popen handle andterminate()on timeout;shlex.split-based un-quote) and a dead/misleading_pty_injectfallback removed. Re-QA PASS, each fix's guard test proven load-bearing by mutation.Not verified here (host-proof, by design)
Real Windows ConPTY (a session actually launching/capturing/driving on a real host, adopt-on-reboot, Session-0 service, job-object breakaway) — this QA host is Linux and node-pty won't build here (no
make). The pty-host itself isagent/win/drive.mjs's 24/24 on real Windows ConPTY (XERK-678);agent/win/pty-control-drive.py(added here) is the host-proof manager-side drive. Flagged to the XERK-678 session (on the real Windows host) for real-host validation.