Skip to content

fix(host): start the phone's conversations on a Windows computer - #57

Draft
Calmzir wants to merge 1 commit into
zavudev:mainfrom
Calmzir:fix/windows-mobile-turns
Draft

Calmzir wants to merge 1 commit into
zavudev:mainfrom
Calmzir:fix/windows-mobile-turns

Conversation

@Calmzir

@Calmzir Calmzir commented Oct 13, 2026

Copy link
Copy Markdown

What and why

Starting a conversation from the phone with a Windows computer as the host fails before the agent runs:

The other computer could not do that. cannot start sh: CreateProcessW `"sh -c \"shell=...\" leon sh -c \"n=$1; shift ...\" leon-bridge 2 ... claude -p --input-format stream-json ..."` in cwd `Some("C:/Users/.../projects/...")` failed: The system cannot find the file specified. (os error 2)

Where it comes from

leon-mobile asks the host to start every turn as a POSIX command: leon_turn::shell::login_argv (the login-shell wrapper) around leon_turn::bridge::argv (the bridge script, which uses mkfifo, stty and a FIFO). The host spawns it as is (leon-pty for the turn's terminal, leon-host::exec for the probes like $HOME/$PWD). Windows has no sh on PATH, so CreateProcessW fails.

With sh found, two more things break on Windows. I checked both on a real ConPTY:

  1. The bridge's marks get lost. They were written between two record separators (0x1E). ConPTY re-renders the output and drops a lone 0x1E, so leon-ready / leon-ack:… arrive as plain text. The phone never sees the bridge is ready, and nothing gets written.
  2. The folder from a probe is an MSYS path. Git's sh prints $HOME/$PWD as /c/Users/me. When no project is shared, that comes back as the turn's cwd, and CreateProcessW cannot use it.

The fix

  • leon-pty::spec::program: on Windows, the program sh is the one Git for Windows ships. It is looked up on PATH first, then next to git.exe on PATH (<Git>\cmd\git.exe → <Git>\bin\sh.exe), then in %ProgramFiles%\Git\bin\sh.exe. Any other program, and sh on any other OS, is started unchanged. Git's sh has everything the bridge and the login wrapper use. Both command_builder_with (terminals) and leon-host::exec (probes) go through it.
  • leon-pty::spec::folder: on Windows, /x or /x/... becomes X:/.... Anything else is kept as it is. Both spawn paths use it.
  • leon-turn::bridge: a mark is now an OSC of its own, ESC ]5379;leon-… BEL, instead of 0x1E leon-… 0x1E. Like 0x1E, ESC and BEL never appear unescaped in a JSON line. Terminals ignore an OSC they do not know, and ConPTY passes it on. Marks opens on ESC and closes on BEL. Anything between them that is not a mark (the shell's or ConPTY's own sequences, such as ESC[6n or ESC]0;title BEL) is now kept as output. Before, it was dropped. leon-mobile's turn engine splits its input on the new closing byte (bridge::CLOSE).

The bridge script travels from the client, and the parser lives in the client, so both ends change together. A phone build with this change works with any host. A host only needs the sh/folder change, which matters only on Windows.

How it was checked

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked (all pass)
  • Operating system(s) tried on: Windows 11 Pro (build 26200), with Git for Windows and Claude Code installed natively

Beyond the unit tests (new: sh_is_found_beside_git_or_in_its_install_folder, a_folder_sh_printed_is_the_drives_on_windows, what_opens_no_mark_is_output_whole), I ran the exact argv the phone sends, sh -c LOGIN_SHELL leon sh -c BRIDGE leon-bridge 1 <json> cat, through PtyProcess::spawn on ConPTY:

  • Before: cannot start sh … (os error 2). With only the sh change, the output shows leon-ready with no fences, and every mark is lost.
  • After: the arguments arrive intact (quotes, $HOME, a 5000-byte line). Output is ESC]5379;leon-ready BEL + the line + ESC]5379;leon-ack:1:end BEL, and it exits 0.
  • Which escape forms survive ConPTY: 0x1E, APC (ESC _ … ST) and DCS are dropped; an unknown OSC passes through.

I have not yet run it end to end from a rebuilt phone app.

Notes for the reviewer

  • Older Windows 10: I confirmed that an unknown OSC passes through ConPTY on Windows 11 build 26200. Older ConPTY builds may filter it. If so, those hosts would still not see the marks, though they would fail no worse than today.
  • Git for Windows is required on the host for phone turns. Without it, the error stays the same as today. A clearer message ("install Git for Windows") could be a follow-up. The phone's other sh probes (home_dir, subdirs, which) benefit from the same lookup.
  • Replays of turns started by an older phone build still carry 0x1E marks, and the new parser treats them as output. That only affects turns that were running during the upgrade.
  • The mobile app (mobile/) needs a rebuild of leon-mobile for the bridge change to reach phones.

A turn started from the phone failed on a Windows host with
"cannot start sh: CreateProcessW ... (os error 2)": the phone asks the
host to run `sh` (the login shell wrapper and the bridge are POSIX), and
Windows has no `sh` on PATH.

* leon-pty: on Windows, `sh` is the one Git for Windows ships (on PATH,
  beside git.exe, or in Program Files) when PATH has none. It has
  everything the bridge uses (mkfifo, stty). A folder `sh` printed
  (/c/Users/me) is started in as C:/Users/me. leon-host's exec uses both.
* leon-turn: the bridge's marks are an OSC (ESC ]5379; ... BEL) instead
  of text between record separators: the Windows pseudo-console drops a
  lone 0x1E, and the phone never saw the bridge was ready. ESC and BEL
  are, like 0x1E, never unescaped in a JSON line; terminals ignore an
  OSC they do not know. Anything else between ESC and BEL stays output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant