feat(queue): dispatch to Windows psmux hosts alongside POSIX remotes - #84
Merged
Conversation
…4 with Console.Out to avoid wrap truncation
LeTuR
force-pushed
the
feat/psmux-host
branch
from
September 13, 2026 05:03
873c14c to
857c947
Compare
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.
Intent
Fleet task remote-host-support/02-02-psmux-and-windows. The operator asked, verbatim: 'Fleet disaptching to a psmux should work, maybe running fleet on windows should work also ?'. The brief required a decision with evidence and the smallest change that makes it real: verify each of the three couplings the old refusal cited (probes, brief copy, result fetch) against the real Windows host win-host (PowerShell 5, psmux 3.3.6), answer A (dispatch TO Windows) and B (fleet RUNNING on Windows) separately, and build A only if contained, as a seam per coupling with the POSIX implementation unchanged and a PowerShell one beside it, driven by a test to the standard scripts/lib/forge.py set (second implementation driven with a tripwire on PATH), proven against win-host. Hard constraints: do not regress the POSIX path (existing remote selftests unchanged), do not weaken the refusal to make the error go away (a half-working spawn would leave a worktree on a Windows box), no new dependency (no PowerShell module, Cygwin or busybox), say what could not be tested. B was to be answered, not implemented. Findings and decisions: all three couplings were real (printf/[ -d ] do not exist in PowerShell; cat > path writes nothing; PS5 stdin/stdout go through the ibm850 code page so non-ASCII does not survive), plus a fourth the refusal never named: the trust handoff (psmux captures panes with spaces removed, so session-trust.sh's signature never matched; Claude Code 2.1.247 on Windows pre-selects 'Yes', so the table's 'down enter' chose 'No, exit' and the agent exited - observed live). Built: a HostShell seam in scripts/lib/queue.py selected by hosts.toml's multiplexer (the same proxy thurbox uses, HostDef::is_windows): PosixShell issues byte-for-byte the previous commands including the /bin/sh -lc login wrap; PowerShell sends UTF-16LE base64 -EncodedCommand (thurbox's own host_powershell_c rule, because -Command quoting is rewritten by the host's default shell), carries file bytes as base64 both directions, joins paths with backslash, and runs the forge probe's ssh key check via bounded Start-Process because inline '& ssh' hangs inside an ssh session on Windows (measured: & ssh, ssh -n and cmd /c <NUL all hung past 45s). Any multiplexer other than tmux/psmux is still refused by name at add; a Windows box that hosts.toml calls tmux is still caught by the first probe. Deliberately, exit codes carry no meaning beyond zero/non-zero and ssh's 255, because Windows folds a PowerShell exit code; answers are words on stdout sharing one line protocol with the POSIX scripts. reportable() strips OpenSSH's post-quantum advisory and PowerShell CLIXML / error trailer so failure reasons name the real error - the only POSIX-visible change is that error text. session-trust.sh now matches whitespace-insensitively and sends Enter alone when the selector already sits on the accepting option; a dialog defaulting to No still gets 'down enter'. Tests in scripts/queue-selftest.sh §11(i)/(j): a fake Windows host (windows-host.py in the ssh stub) decodes every -EncodedCommand, moves real bytes into a fake C:\ tree, and logs any non-encoded (POSIX) command as a tripwire, with a claim proving the tripwire fires; §11(a-h) unchanged except the refusal row now uses oddbox (unknown multiplexer) instead of winbox. The Windows reap claim holds its topic open with a condition-blocked task because of a pre-existing, deliberately unfixed bug: a topic whose last remote task lands while its host is down archives in the same pass and reap then never revisits the kept session. Known limits, accepted: thurbox gates hook status off on psmux hosts so Windows workers stay 'unreported' and reap never releases them; win-host has no GitHub credentials so no full worker run was possible; GitLab origin, non-22 ssh port and PowerShell 7 on Windows are untested. B answered in docs/PR only: yes with WSL, native is a port of 22 bash scripts plus the setsid supervisor. Docs updated: scripts/queue.sh header, fleet-queue and thurbox-session skills. This run exists to attest PR #80 (task was queued as 'pr' by the #74 bug; the operator's standing method is attested).
What Changed
HostShellseam inscripts/lib/queue.py(PosixShellplus a newPowerShellimplementation) so remote dispatch's three probes, brief copy, and result fetch each speak the shell a host actually runs: POSIX for atmuxhost (byte-for-byte unchanged, including the/bin/sh -lclogin wrap), PowerShell for apsmux(native Windows) host via UTF-16LE base64-EncodedCommand, base64 file transfer in both directions, backslash path joining, and a boundedStart-ProcessSSH key check to avoid an inline& sshhang observed on Windows.host_entry()now refuses any multiplexer other thantmux/psmuxby name instead of refusing every non-tmuxhost.reportable()to strip OpenSSH's post-quantum advisory and PowerShell's CLIXML/error-trailer noise from probe output so reported failures name the real error, on both POSIX and Windows hosts.scripts/session-trust.shto match trust-dialog signatures with whitespace stripped from both sides (psmux captures panes with spaces removed) and to sendEnteralone whenever the dialog's selector is already on the accepting option, instead of always sending the fixeddown entersequence that would otherwise pick "No, exit" on Claude Code's Windows-defaulted dialog.scripts/queue-selftest.shwith a fake Windows host (decoding-EncodedCommand, writing into a fakeC:\tree, and tripwiring any non-encoded POSIX command) covering the new PowerShell path, updated the existing refusal-row test to use an unknown multiplexer name instead of assuming any non-tmuxhost is refused, and updatedscripts/queue.sh's header comment plus thefleet-queue/thurbox-sessionskill docs to describe the two-shell behavior.Risk Assessment
✅ Low: The fix-round change (scripts/lib/queue.py:2515, switching to [Console]::Out.Write) correctly bypasses PowerShell's console-width formatter, resolving the line-wrap truncation risk flagged in round 1; the rest of the HostShell seam, session-trust.sh's whitespace-insensitive/selector-aware matching, and the behavioral (non-string-matching) selftest coverage are consistent, well-scoped, and match the stated intent with no regression to the POSIX path.
Testing
Baseline ./scripts/check.sh (full gate, including the complete queue-selftest.sh with its Windows/psmux dispatch and result-fetch coverage) already passed with no regressions. Because that suite's fake Windows-host stub doesn't model PowerShell's real non-interactive line-wrapping and no PowerShell interpreter is available on this machine, I wrote and ran a focused manual verification against the real decode()/last_out_of() functions using the actual pre-fix and post-fix read() output shapes and the suite's own realistic result.md fixture: the pre-fix shape silently decodes to wrong bytes (the reported bug, reproduced), and the post-fix shape decodes to the exact original bytes, confirming the fix closes the gap for a realistic payload.
Evidence: Manual verification script (models pre-fix vs post-fix PowerShell output shapes through the real decode/last_out_of)
Evidence: Verification run output: pre-fix shape decodes to wrong bytes, post-fix shape decodes exactly
fixture: 137 raw bytes -> 184 base64 chars (> 80-column buffer width, so wrapping would trigger) --- pre-fix behavior model (bare expression -> Out-Default wraps) --- stdout lines: 3 decode() result: silently WRONG (truncated/garbage) bytes -- the bug --- post-fix behavior model ([Console]::Out.Write -> single line) --- stdout lines: 1 decode() result matches original 137-byte result.md exactly: TruePipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
scripts/lib/queue.py:2515- PowerShell.read() emits the base64 payload via the pipeline's implicit output ([Convert]::ToBase64String(...)as the script's last expression), which goes through PowerShell's defaultOut-Default/console-formatting layer. When powershell.exe runs non-interactively over an exec-channel ssh session (no pty is allocated here — ssh_argv has no-t), it cannot query a real console width and falls back to a default buffer width (commonly 80), and that formatter wraps long string output at that width even though it's being redirected to a pipe, not a terminal — a well-known PowerShell gotcha for exactly this invocation shape.PowerShell.decode()(line 2518-2522) recovers the payload withbase64.b64decode(last_out_of(stdout), ...), andlast_out_of(line 2235) returns only the LAST non-empty line of stdout. Any result.md whose base64 encoding exceeds one wrapped line (roughly >60 raw bytes — true of essentially any real task result, e.g. the test's own ~140-byte fixture result) would have its earlier lines silently discarded, leaving only the tail chunk to decode. This mechanism isn't exercised by the new queue-selftest.sh (i) coverage because the fake windows-host.py stub emits the base64 with a single Pythonprint()and never simulates PowerShell's own line-wrapping, so the gap is untested. Depending on the wrapped tail's length modulo 4, decode() will usually raise ValueError (safe, loud failure — fetch_result reports "the host answered, but not with the file's bytes") but could also occasionally decode to truncated-but-valid-looking bytes, silently returning a wrong (truncated) result. Either way this would make the freshly-built "result fetch" coupling non-functional for realistic Windows task outputs, contradicting the intent's claim that this coupling was verified end-to-end. The fix is mechanical: have the PowerShell script write the base64 string directly to the output stream (e.g.[Console]::Out.Write(...)) instead of relying on the formatted pipeline output, bypassing the width-based wrapping entirely.🔧 Fix: fix(queue): write PowerShell result base64 with Console.Out to avoid wrap truncation
✅ Re-checked - no issues remain.
./scripts/check.sh./scripts/check.sh (baseline, already run successfully before this phase - includes the full scripts/queue-selftest.sh, notably section 11(i)/(j) Windows/psmux dispatch and result-fetch coverage)git show 5efbde3:scripts/lib/queue.py and git show 873c14c:scripts/lib/queue.py - diffed PowerShell.read() across the pre-fix and post-fix commits to confirm the exact code shapes exercised belowpython3 verify_wrap_fix.py - manual verification importing the real queue.py PowerShell.decode()/last_out_of(), feeding it a modeled pre-fix (wrapped) vs post-fix (unwrapped, [Console]::Out.Write) stdout for the real ~137-byte non-ASCII result.md fixture used by queue-selftest.sh section 11(i)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.