Skip to content

fix(wsl): hold a WSL pane's startup command until the guest shell is ready - #24223

Open
punleon wants to merge 2 commits into
stablyai:mainfrom
punleon:fix/automation-wsl-prompt-ready
Open

punleon wants to merge 2 commits into
stablyai:mainfrom
punleon:fix/automation-wsl-prompt-ready

Conversation

@punleon

@punleon punleon commented Sep 30, 2026 •

Copy link
Copy Markdown

ELI5

When an automation (or any agent launch) starts an agent in a WSL terminal, Orca typed the command before WSL had finished starting. Some shell setups throw away anything typed that early, so the agent never started. Now Orca waits until the shell inside WSL says it is ready, then types the command.

What Changed

Before: the terminal shows the command echoed at the top, then a fresh prompt, and nothing runs (the screenshot in #24188: pi 'test' above ➜ rcm).

After: the command is typed at the prompt and runs.

Mechanism. On Windows the host wrote the startup command into wsl.exe about 30 ms after the first output. At that point the distro may still be booting and zsh/bash is still reading its config. Inside the guest, Orca already starts bash and zsh through its shell-ready wrappers, and those emit the OSC 777 ready marker when ORCA_SHELL_FEATURES contains ready. The host just never asked for it across wsl.exe.

  • A WSL pane with a typed startup command now gets ORCA_SHELL_FEATURES=ready, passed through WSLENV, and the host holds the write until the marker arrives. If the marker never comes, the command is written after 15 s anyway, the same cap the daemon already uses.
  • Covers both transports: the in-process provider (local-pty-finalize-environment.ts) and the terminal daemon (shell-launch-plan.ts asks for the marker, terminal-host-session-create.ts queues the write). Both use one predicate, wslStartupCommandWaitsForShellReady. The daemon has to decide after spawn, because main can't tell a pane will be wsl.exe.
  • Only a prompt hook reports ready, so the marker can't arrive before the shell's config has run. Guest fish, which Orca doesn't wrap, is started with the same fish_prompt hook host fish uses. Other unwrapped shells (sh/dash, bash/zsh with a missing wrapper) report nothing, and the command goes out at the 15 s cap.
  • WSL writes stay raw (no bracketed paste), because the guest may be a shell that doesn't handle it.

Plain WSL terminals without a startup command, and every non-WSL shell, are unchanged.

Why

This reuses the readiness handshake Orca already has for local bash/zsh/fish. No new wire fields, no renderer changes. Alternatives I rejected:

  • A fixed delay: WSL cold start ranges from well under a second to several seconds.
  • Handing the command to the guest wrapper through env (the startup feature): an unwrapped guest shell would silently drop it.

#23962 and #24257 (both merged) change how long launch lines and prompts arrive, but the launch line is still typed into the shell as soon as the pane starts, so they don't cover this. #23748 lists "WSL gets no shell check" as a known gap.

Linked Issue

Fixes #24188

Visual Proof

End-to-end run on Windows 11 with a real ConPTY and wsl.exe into Ubuntu 26.04: zsh + oh-my-zsh, WSL shut down before each run, startup command echo ORCA_$((20+22))_DONE. To reproduce the reporter's lost input, .zshrc got one line that drains pending tty input at startup, which stands in for plugins or update prompts that read the tty.

where the command was typed result
in-process, before before the first prompt (byte 23 vs prompt at 325) lost, nothing ran within 30 s
in-process, after after the prompt (297 vs 262) ran, ORCA_42_DONE at ~5 s
daemon, before before the prompt lost
daemon, after after the prompt ran at ~2.6 s
guest fish, config.fish with sleep 2, after after the prompt ran at ~8.6 s in-process, ~5.8 s daemon
unwrapped guest (dash), after after the 15 s cap ran at ~15 s

With stock oh-my-zsh (no draining line) the command is also typed before the prompt, but zsh keeps it, which is why the bug depends on the setup.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Daemon: terminal-host-startup.test.ts checks that the command is held until the marker, and delivered on timeout. Fails without the fix.

  • In-process: local-pty-provider-windows-shell-launch.test.ts checks the env and WSLENV request and that nothing is written before the marker. Fails without the fix.

  • Login script: wsl-login-shell-command.test.ts runs the shell dispatch under a real sh: the script never prints the marker itself, fish gets the prompt hook only when ready was requested, a hookless shell never sees ORCA_SHELL_FEATURES, and wrapped shells are left alone. wsl-startup-shell-ready.test.ts covers the predicate and checks that the marker matches SHELL_READY_MARKER_ESCAPED.

  • Ran on Windows and on Linux (Ubuntu in WSL). Across src/main/providers, src/main/daemon, src/main/pty the set of failing tests is the same before and after, apart from two real-shell timing tests that failed once under full parallel load and passed 3/3 in isolation. uses the preferred WSL distro for Windows cwd WSL terminals fails on current main too (the HISTFILE assertion), on both platforms. A full pnpm test in WSL showed 7 more failing files outside these folders; run one by one, each gives the same result on main.

  • tsc -p config/tsconfig.node.json, oxlint, oxfmt and check:code-quality:changed are clean.

  • macOS not tested; the new code only runs when process.platform === 'win32' and the shell is wsl.exe.

AI Disclosure

Written with Claude Code (Claude Opus 5.5): root-cause analysis, the patch, the tests and the WSL end-to-end runs. I reviewed the change and the test results.

Review

AI self-review (Claude Code), re-run after rebasing onto e04c2f9e:

  • Cross-platform: the new code runs only when process.platform === 'win32' and the shell is wsl.exe. macOS and Linux take the existing paths.
  • SSH/remote: no change. SSH panes keep their own marker wait.
  • Agents/integrations: provider-neutral. It holds whatever startup command the pane has. Guest fish keeps main's claude function (Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes #26801) next to the ready hook.
  • Performance: non-WSL panes do no extra work. A WSL startup command waits for the marker, capped at 15 s.
  • Security: no new inputs. The marker is the existing OSC 777 sequence, and the fish hook is a fixed string passed through quotePosixShell.
  • UI: none.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: untouched. SSH panes already wait for the marker on their own path.
  • Backwards compatibility: an older daemon that is still running keeps its old behaviour until it restarts. The request and the wait both live inside the daemon, so they can't disagree.
  • Performance: at most one extra 30–200 ms settle after the marker for WSL startup commands.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)
    • Local, Linux (Ubuntu in WSL): pnpm lint passes. Typecheck passes, each project run alone with --singleThreaded; the parallel run runs out of memory in this 7.7 GB VM. Every pnpm build step passes except build:mobile-web, which needs the mobile/ dependencies that aren't installed here; this PR touches nothing in mobile/.
    • pnpm test: the affected folders pass. The full run couldn't finish in WSL, and every file that failed there fails the same way on main. CI covers the rest.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

On Windows, startup commands for eligible WSL shells can wait for a guest-shell ready marker before submission. The launch environment requests the marker, and the WSL login-shell command configures marker reporting. The readiness session uses a configurable timeout, set to 15 seconds for this WSL path. The daemon recognizes WSL readiness while keeping bracketed-paste behavior tied to wrapped-shell readiness.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 6751b

The readiness change has bounded fallback behavior, but the new tests fail on Windows installations without host sh. Add an availability guard to keep Windows test runs portable.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 11 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Issue #24188 requires WSL startup input to wait until the terminal can accept it. The PR adds shell-ready marker waiting for Windows WSL startup commands in both the in-process provider and terminal d…
Out of Scope Changes check Passed The changes remain connected to issue #24188. They add WSL readiness selection, marker propagation through WSLENV, guest-shell marker emission, timeout handling, and focused tests. The reported behavi…
Title check Passed The title clearly and concisely describes the main change: delaying WSL pane startup commands until the guest shell is ready.
Description check Passed The description is complete and relevant. It explains the user impact, implementation, rationale, linked issue, testing, platform scope, compatibility, and checklist status. The visual proof is textua…
Full details: Linked Issues check

Explanation

Issue #24188 requires WSL startup input to wait until the terminal can accept it. The PR adds shell-ready marker waiting for Windows WSL startup commands in both the in-process provider and terminal daemon. It sends the command after the marker or after a 15-second timeout. Tests cover marker waiting, timeout delivery, login-script behavior, and the shared predicate. The implementation addresses shell readiness; the issue does not require a separate agent-readiness signal.

Full details: Out of Scope Changes check

Explanation

The changes remain connected to issue #24188. They add WSL readiness selection, marker propagation through WSLENV, guest-shell marker emission, timeout handling, and focused tests. The reported behavior leaves plain WSL terminals without startup commands and non-WSL shells unchanged. No unrelated change is demonstrated.

Full details: Description check

Explanation

The description is complete and relevant. It explains the user impact, implementation, rationale, linked issue, testing, platform scope, compatibility, and checklist status. The visual proof is textual rather than an attached screenshot or video, but the required behavior evidence is documented.

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 9b3b50bd-f1d6-4006-bcfa-426a13200638

📥 Commits

Reviewing files that changed from the base of the PR and between afa81dc and 3bda7f0.

📒 Files selected for processing (11)
  • src/main/daemon/pty-subprocess/shell-launch-plan.ts
  • src/main/daemon/terminal-host-session-create.ts
  • src/main/daemon/terminal-host-startup.test.ts
  • src/main/providers/local-pty-finalize-environment.ts
  • src/main/providers/local-pty-provider-windows-shell-launch.test.ts
  • src/main/providers/local-pty-shell-readiness-session.ts
  • src/main/providers/local-pty-shell-ready.ts
  • src/main/pty/wsl-startup-shell-ready.test.ts
  • src/main/pty/wsl-startup-shell-ready.ts
  • src/shared/wsl-login-shell-command.test.ts
  • src/shared/wsl-login-shell-command.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread src/shared/wsl-login-shell-command.ts Outdated
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Changes how WSL terminal startup commands are delivered.

The PR appears safe to merge, though startup commands in unwrapped WSL shells will be delayed by 15 seconds.

Findings

  1. P2 Unwrapped shells wait 15 seconds ▶

Summary

The PR requests a guest-shell readiness marker before delivering typed WSL startup commands in both terminal transports. The follow-up moves fish reporting to its prompt hook and stops unwrapped shells from reporting ready before their startup files run.

  • Unwrapped guest shells now incur the full readiness timeout for each typed startup command.

Reviews (2) · Last reviewed commit: "fix(wsl): report guest readiness only fr..."

Comment thread src/shared/wsl-login-shell-command.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • WSL readiness predicate — new src/main/pty/wsl-startup-shell-ready.ts gates the wait on win32 + wsl.exe + a typed (not argv-delivered) startup command, and requests the existing OSC 777 marker via ORCA_SHELL_FEATURES=ready (WSLENV).
  • Daemon path — shell-launch-plan.ts requests the marker; terminal-host-session-create.ts derives wslShellReadySupported from the spawned subprocess (main's win32 shell-ready hint is always false), ORs it into shellReadySupported, and keeps WSL writes raw.
  • In-process path — local-pty-finalize-environment.ts overrides plan.shellReadyLaunch with supportsReadyMarker: true and a 15 s readyTimeoutMs; local-pty-shell-ready.ts and the readiness session thread that timeout through.
  • Guest login script — WSL_UNWRAPPED_SHELL_READY_BLOCK fires the marker before the final exec for shells Orca does not wrap, gated by _orca_wsl_shell_wrapped so wrapped bash/zsh self-report and consume the channel.
  • Tests — new daemon, in-process, predicate, request, and real-sh login-script coverage; I ran the changed suites plus session, shell-ready, windows-shell-args, and wsl-orca-env locally (all green).

I traced both transports end to end: the in-process override survives local-pty-spawn.ts because getFallbackShellReadyConfig is only set on the non-win32 branch, WSL never embeds its command in shell args (buildWslShellArgs ignores startupCommand), and addOrcaWslInteropEnv already publishes ORCA_SHELL_READY_ROOT, so the marker arrives from either the wrapper or the login script. The bracketed-paste change is not a regression for WSL (the flag was already false there) and is left unchanged for non-WSL shells.

ℹ️ Unwrapped guest shells report ready before their rc runs

For fish/sh/dash (or bash/zsh with a missing wrapper), WSL_UNWRAPPED_SHELL_READY_BLOCK emits the marker immediately before exec "$_orca_wsl_shell" -l, so the held write lands while the guest's startup files are still running. That still cures the reported WSL-boot race, but a config that drains tty input at startup can drop the command in this fallback path. The wrapped bash/zsh path (marker from the line editor at the prompt) is not affected, and wrappers normally exist for those shells — noting it only so the limitation is on the record, no change requested.

Technical details
# Unwrapped-shell marker precedes login rc

## Affected sites
- `src/shared/wsl-login-shell-command.ts:158` — `WSL_UNWRAPPED_SHELL_READY_BLOCK` runs before `exec "$_orca_wsl_shell" -l`, so zsh/bash rc files execute after the marker.
- `src/main/pty/wsl-startup-shell-ready.ts` — the host writes ~30–200 ms after the marker; an rc that reads/drains the tty in that window still loses the command.

## Required outcome
- None requested. Documented limitation: the wrapped path reports from the prompt (correct); unwrapped is best-effort.

## Open questions for the human
- Is the unwrapped fallback's early marker acceptable given wrappers are generated for bash/zsh, or should fish/sh be treated differently (e.g. don't hold)?

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/shared/wsl-login-shell-command.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 99cced72-2bd6-4f0c-9aff-e94546aa83c7

📥 Commits

Reviewing files that changed from the base of the PR and between 3bda7f0 and 6751b98.

📒 Files selected for processing (4)
  • src/main/pty/wsl-startup-shell-ready.test.ts
  • src/main/pty/wsl-startup-shell-ready.ts
  • src/shared/wsl-login-shell-command.test.ts
  • src/shared/wsl-login-shell-command.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/pty/wsl-startup-shell-ready.ts
  • src/main/pty/wsl-startup-shell-ready.test.ts
  • src/shared/wsl-login-shell-command.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread src/shared/wsl-login-shell-command.test.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Incremental re-review of commit 6751b98 on top of the previously reviewed 3bda7f0; it reworks _orca_wsl_shell's readiness reporting so a marker can only come from a prompt hook, addressing the prior review's noted unwrapped-shell limitation. Four files changed.

  • Prompt-hook-only readiness — removed the pre-exec WSL_UNWRAPPED_SHELL_READY_BLOCK; bash/zsh wrappers still self-report, and fish now starts via fish -l -C <WSL_FISH_SHELL_READY_INIT>, whose --on-event fish_prompt handler fires after config.fish.
  • Channel consumption — added _orca_wsl_shell_wrapped, set only by the zsh fall-through path, and a final [ -n "$_orca_wsl_shell_wrapped" ] || unset ORCA_SHELL_FEATURES; wrapped bash execs into its rcfile and consumes the channel there, so ORCA_SHELL_FEATURES cannot leak into the user's shell.
  • Unwrapped shells — sh/dash/missing-wrapper bash-zsh no longer emit an early marker and instead fall back to the host's 15 s cap.
  • Tests — replaced the unwrapped-block suite with dispatch tests over the real login script (fish args, hookless timeout, wrapped zsh) and asserted WSL_FISH_SHELL_READY_INIT equals the host's getFishShellReadyInitCommand.

Prior pullfrog feedback (the unwrapped-shell early-marker note) is addressed by this commit: readiness is now only reported from a prompt position, which is exactly what the note asked to put on record.

ℹ️ PR description still documents the removed fallback

The PR body's "What Changed", "Testing", and visual-proof table describe an unwrapped-shell marker emitted from the login script right before exec, and claim an unwrapped dash guest runs "at ~3.4 s, no 15 s stall". Commit 6751b98 deleted that path: only bash/zsh wrappers and fish's prompt hook report ready, while sh/dash/missing-wrapper shells now wait out the full 15 s cap. The code is fine — the description just no longer matches what it ships, so please update it (and confirm the timeout-fallback tradeoff is intended).

Technical details
# Stale PR description after the prompt-hook rework

## Affected sites
- `src/shared/wsl-login-shell-command.ts:140-153` — the fis case reports at the prompt; everything else falls through to the final consume + `exec`.
- `src/main/providers/local-pty-finalize-environment.ts:54-59` — unwrapped WSL shells inherit `readyTimeoutMs: WSL_SHELL_READY_TIMEOUT_MS` (15 s), replacing the old fast path.
- `src/shared/wsl-login-shell-command.ts:163-169` — `WSL_UNWRAPPED_SHELL_READY_BLOCK` no longer exists.

## Required outcome
- PR description's mechanism, testing bullet, and visual-proof table reflect prompt-hook-only readiness and the 15 s fallback for hookless shells.

## Open questions for the human
- Is the 15 s hold acceptable for `sh`/`dash`/missing-wrapper panes, or should those be treated as non-waiting?

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@punleon
punleon force-pushed the fix/automation-wsl-prompt-ready branch from 6751b98 to c096ce3 Compare October 11, 2026 21:39
@punleon

punleon commented Oct 11, 2026

Copy link
Copy Markdown
Author

Hi @AmethystLiang! This one is ready for a look.

I rebased it onto current main and resolved the conflict with the startup-command staging from #23962. Main also gave guest fish its own claude function (#26801), so fish now starts with both that function and the fish_prompt ready hook in a single -C. All bot review threads are addressed.

Tested locally: typecheck, oxlint and oxfmt are clean, and the changed and related suites pass. The only Windows failures I see (startup-command-staging, one terminal-host staging test, windows-msys-job) fail the same way on a clean main.

The CI workflows are waiting for maintainer approval since I'm an outside contributor. Could you approve them when you have a moment? Thanks!

punleon and others added 2 commits October 19, 2026 22:44
…ready

On Windows, Orca typed the startup command into wsl.exe about 30 ms after
the first output, while the distro was still booting and the login shell
was still reading its config. A config that reads or flushes pending input
(zsh plugins, update prompts) dropped it: the command showed echoed above
a fresh prompt and nothing ran (stablyai#24188).

The guest login script already starts bash and zsh through Orca's wrappers,
which emit the OSC 777 ready marker when ORCA_SHELL_FEATURES asks for it,
but the host never asked across wsl.exe. Now both the in-process provider
and the terminal daemon request the marker for a WSL pane with a typed
startup command and hold the write until it arrives (15 s cap, then the
command is written anyway). A guest shell Orca does not wrap gets the
marker from the login script right before exec, so it waits out the WSL
boot without sitting through the cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review feedback: the fallback marker for shells Orca does not wrap came
before the shell's own startup files, so a config that reads pending input
could still eat the startup command. Guest fish now starts with the same
fish_prompt hook host fish uses (after config.fish); any other unwrapped
shell sends no marker and the host falls back to its 15 s timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@punleon
punleon force-pushed the fix/automation-wsl-prompt-ready branch from c096ce3 to b9961d0 Compare October 19, 2026 22:24
@punleon

punleon commented Oct 19, 2026

Copy link
Copy Markdown
Author

Hi @nwparker! Could you approve CI for this one? It fixes #24188: an agent command gets lost when an automation starts it in a WSL terminal.

I rebased it onto current main (e04c2f9e). The only conflict was a test import, after quotePosixShell moved to shell-argument-quoting. After the rebase, tsc (node and cli), the affected tests, oxfmt and check:code-quality:changed all pass. One test in that set, resolves the wrapper root from ORCA_USER_DATA_PATH, fails, and it fails the same way on main on Windows.

This branch has not been deployed

No deployments
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.

[Bug]: Automation sends agent prompt before WSL terminal is ready

1 participant