Skip to content

fix(wsl): a passive restore never starts a stopped WSL distro; its bridge panes wait with Start - #35813

Merged
nwparker merged 2 commits into
mainfrom
nwparker/wsl-bridge-passive-restore-gate
Oct 29, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/wsl-bridge-passive-restore-gate

Conversation

@nwparker

@nwparker nwparker commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​364 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​362
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​358 $\color{#cf222e}{\Huge{\mathbf{−}}}$​61 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​297

ELI5

When Orca opens, reloads, or re-mounts a terminal on its own, a WSL bridge pane used to start its shell straight away. If that pane's WSL distro was stopped, starting the shell (wsl.exe) booted the whole distro without you doing anything. Now such a pane shows its saved output and a " is stopped" card with Start. The distro starts only when you press Start, open New Terminal, or switch to the workspace yourself after it stopped.

What Changed

  • Before:
    • Mounting a restored bridge pane always called wsl.exe, so the distro booted at launch, on reload, or when Orca re-mounted a pane, even after it had stopped.
    • Today this is hidden only because a stopped distro's worktree list is empty. fix(wsl): keep a stopped distro's worktrees and re-list once it runs #35733 fixes that list, which would expose the bug, so the two land together.
  • After:
    • A restore that no user act asked for, on a distro a settled read confirms stopped, is held:
      • no process starts;
      • the saved screen is painted read-only;
      • the shared stopped card with Start appears.
    • Start reconnects every pane held for that distro as the user's act, which starts it.
    • The pane starts only when a settled read proves the distro running. An unanswered read (unverifiable) holds it too, as every other E9 gate does, and Start still recovers it.
  • Mechanism:
    • Renderer:
      • New lib/workspace-switch-user-act.ts, shared with wsl-e9-preflight's wake fix. It records the user's own switches to a workspace: activations whose caller passes navigationIntent: 'user-open' (sidebar, Cmd+J and the switcher, shortcuts, the task and PR pages), and picking a terminal tab.
      • Programmatic activations never record one. That includes the CLI/agent orca worktree event, the delete successor, launch restore, reloads and remounts.
      • A restore (deferred-session-reattach-connect.ts) may start the distro only if it is this pane's first mount after such a switch, within 10 s of it. That uses takeWorkspaceSwitchWake(key, 'pane-restore:<pane>', { withinMs }), which is once per switch. Start also may. Every other restore sends wslPassiveRestore: true.
    • Main:
      • The window spawn IPC forwards both fields.
      • DaemonPtyAdapter checks before creating anything (daemon-wsl-passive-restore-hold.ts). A passive restore is held unless readWslDistroRunningState(..., { retryUnverifiable: true }) says running. The hold throws terminal_held_wsl_distro_stopped:<distro>.
      • So a switch at 10:00 can't boot a distro that stopped at 12:00 when Orca remounts the pane at 12:01: that remount is passive. This doesn't depend on Orca having noticed the stop (WSL's idle stop and wsl --shutdown aren't noted).
    • Hold UI:
      • Mirrors the existing newer-daemon hold: holdForStoppedWslDistro keeps the pane read-only.
      • It paints the history through the existing history read (readNewerDaemonHeldScreen, which reads this machine's daemon history for the session).
      • WslDistroStoppedHoldCard renders the card. Start releases every pane held for that distro.
    • Card: WslDistroStoppedCard is the one stopped card. The host panes' WslHostStoppedCard now renders through it, with the same copy and the same Start label, so there are no new strings.
  • What doesn't change:
    • New Terminal and splits (fresh sessions).
    • Paired-server panes: they don't send the fields, because their host decides.
    • Non-WSL panes.
    • Older windows: no fields means the old behaviour.

Why

  • Product rule (lead): passive means launch restore of the active or visible workspace, reload, and Orca's own remounts. Explicit means a user switch or tab pick after launch, Start, and New Terminal.
  • Why a one-shot, recent switch: a "the user ever opened this workspace" flag would let a later remount boot a distro that stopped afterwards. Comparing the switch with the last noted stop misses stops nobody notes. A switch that permits only the mounts it causes has neither hole.
  • Why hold through the spawn path's error: the newer-daemon hold already does this, so it reuses that rollback and main never registers a process that doesn't exist.

Follow-up (not in this PR): a fresh terminal the CLI creates (orca terminal create) in a workspace whose distro is stopped still starts it.

Flag-off: applies to WSL bridge panes, which exist with ORCA_INTERNAL_WSL_HOSTS unset too. A passive restore whose distro is confirmed stopped now waits for Start instead of booting the distro. Everything else is unchanged.

User risk: low to medium. After a relaunch with the distro stopped, a bridge pane waits for Start instead of starting by itself. It's never held when the distro runs, or when the read is inconclusive.

Linked Issue

Internal: E9 launch auto-mount boot (found in verify-18 part 2), sequenced with #35733.

Visual Proof

N/A in this PR: the card is the existing stopped card (#35168), reused. A device check is planned for wsl-verify-7 part 3.

Testing

  • daemon-pty-adapter-wsl-passive-restore-hold.test.ts, real DaemonServer, win32 + \\wsl.localhost cwd:
    • passive + stopped is held with nothing spawned;
    • passive + unverifiable is held;
    • running spawns;
    • Start and New Terminal skip the read;
    • non-WSL is untouched.
  • deferred-session-reattach-wsl-distro-stopped-hold.test.ts:
    • launch sends passive, is held read-only with the saved screen, and shows no error toast or fresh shell;
    • Start reconnects without the passive flag;
    • the pane a user switch mounts starts;
    • switch, then a remount hours later, is passive (so a stopped distro holds it);
    • a pane first mounted long after the switch is passive;
    • a server pane sends nothing.
  • workspace-switch-user-act.test.ts:
    • markVisited records only with 'user-open';
    • takeWorkspaceSwitchWake is once per switch per consumer, and honours withinMs.
  • worktree-activation-automation-filter.test.ts: activations without navigationIntent (the CLI event's and the delete successor's options) record nothing; 'user-open' records one. A mutation that always records fails it.
  • worktree-event-runtime-activation.test.ts and active-worktree-removal-successor.test.ts: both callers pass no navigationIntent.
  • spawn-options-spawn-intent.test.ts: IPC forwards only true.
  • pnpm tc, oxlint on the changed files, check:code-quality:changed and the ast-grep gates pass.
  • The terminal-pane, renderer lib and tab-bar suites pass 13195; one test (pty-connection-hidden-replay) failed under full load and passes alone (94/94).
  • src/main/daemon, src/main/ipc/pty and the gate tests pass 4618.

Review

Terminal restore, so two reviewers: fix-reviewer and e9-reviewer.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

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)

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2ee82f15-0724-49cb-bd9d-529e605acfa6
Commits

Reviewing files that changed from the base of the PR and between 7d0982d and 8f1f3ba.

Files selected for processing: 28
  • src/main/daemon/daemon-pty-adapter-wsl-passive-restore-hold.test.ts
  • src/main/daemon/daemon-pty-session-spawn.ts
  • src/main/daemon/daemon-wsl-passive-restore-hold.ts
  • src/main/ipc/pty/ipc/spawn-options-spawn-intent.test.ts
  • src/main/ipc/pty/ipc/spawn-options.ts
  • src/main/ipc/pty/ipc/spawn-types.ts
  • src/main/providers/pty-provider-contract.ts
  • src/preload/api/pty-api.ts
  • src/preload/api/pty-bridge-session-control.ts
  • src/renderer/src/components/tab-bar/use-tab-bar-item-actions.ts
  • src/renderer/src/components/terminal-pane/TerminalPaneRuntimePortals.tsx
  • src/renderer/src/components/terminal-pane/WslDistroStoppedCard.tsx
  • src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.tsx
  • src/renderer/src/components/terminal-pane/WslHostStoppedCard.tsx
  • src/renderer/src/components/terminal-pane/ipc-pty-spawn-request.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts
  • src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-wsl-distro-stopped-hold.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection/wsl-distro-stopped-hold-pane.ts
  • src/renderer/src/components/terminal-pane/pty-transport-types.ts
  • src/renderer/src/components/terminal-pane/wsl-distro-stopped-hold-state.ts
  • src/renderer/src/hooks/ipc-events/worktree-event-runtime-activation.test.ts
  • src/renderer/src/lib/active-worktree-removal-successor.test.ts
  • src/renderer/src/lib/workspace-switch-user-act.test.ts
  • src/renderer/src/lib/workspace-switch-user-act.ts
  • src/renderer/src/lib/worktree-activation-automation-filter.test.ts
  • src/renderer/src/lib/worktree-activation-visit.ts
  • src/renderer/src/lib/worktree-activation.ts
  • src/shared/wsl-distro-stopped-hold.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The change affects restored WSL bridge terminals in Orca.

The PR should not merge yet because unrelated floating-panel clicks can authorize a distro start, and Start can release panes from different distros.

Findings

  1. P1 Floating clicks mark another workspace ▶
  2. P1 Start groups different distros together ▶
  3. P2 Cmd+J selection still needs Start ▶
  4. P2 Picking the tab leaves it held ▶

Summary

The PR keeps passive WSL terminal restores from starting a distro and adds a shared Start card.

  • Automatic terminal restores wait when a WSL distro is not confirmed running.
  • Held WSL panes keep their saved screen until you press Start.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Restore a local WSL pane] --> B{Start or recent unused user switch?}
  B -->|Yes| C[Connect normally]
  B -->|No| D[Read distro running state]
  D -->|Running| C
  D -->|Stopped or unanswered| E[Show saved output and Start]
  E -->|User presses Start| C
Loading

Reviews (2) · Last reviewed commit: "fix(wsl): a restore may start the distro..." · Reviewed by Greptile

function activateRealTab(activate: () => void): void {
clearClientHostedBrowserRowSelection()
// Picking a tab is the user acting on its workspace: its restore may start a stopped distro.
noteUserWorkspaceSwitch(useAppStore.getState().activeWorktreeId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Floating clicks mark another workspace

Clicking a tab in the floating panel records a switch for the main window's workspace instead of the floating workspace. If the main workspace's WSL distro stopped earlier, a later automatic remount receives this unrelated click's timestamp and can start that distro without the user opening it. Use latest.current.props.worktreeId, which already identifies the clicked tab's workspace.

Suggested change
noteUserWorkspaceSwitch(useAppStore.getState().activeWorktreeId)
noteUserWorkspaceSwitch(latest.current.props.worktreeId)

Comment on lines +14 to +15
lastSwitchAt.set(workspaceKey, Date.now())
switchCounts.set(workspaceKey, (switchCounts.get(workspaceKey) ?? 0) + 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Picking the tab leaves it held

Picking an already-selected held terminal tab only updates these maps. The unchanged selection leaves the pane mounted, so no new connection reads the timestamp. Only Start releases the existing hold. Retry the held pane on explicit terminal selection too, so users do not need an extra Start click.

if (start === -1) {
return null
}
const distro = message.slice(start + WSL_DISTRO_STOPPED_HOLD_ERROR.length + 1).split(/\s/)[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Start groups different distros together

The parser cuts distro names at the first space, so Ubuntu Test becomes Ubuntu. This changes the card label and the group released by Start. If both distros have held panes, pressing Start for either releases both groups and can start the other distro too. Encode the distro name in the error message and decode it without losing spaces.

if (state.activeView !== 'terminal') {
state.setActiveView('terminal')
}
noteUserWorkspaceOpen(worktreeId, opts?.navigationIntent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Cmd+J selection still needs Start

Requiring navigationIntent: 'user-open' misses terminal selections from Cmd+J. activateWorkspaceTabPaletteResult calls activateAndRevealWorkspace without that option, then selects the terminal directly without using the tab-bar handler. Opening a restored terminal in a stopped WSL distro therefore shows Start instead of starting it as the user's explicit selection. Pass the intent from this user-driven path.

nwparker added a commit that referenced this pull request Oct 29, 2026
A passive restore or remount reaches launch preparation too (#35813), so it no
longer runs as a user act; a stopped distro skips the install and the relay
installs once the distro runs. A skipped WSL home now returns not_installed
instead of falling through to the host writer, which would have written the
WSL home over UNC.
@nwparker
nwparker merged commit 598b721 into main Oct 29, 2026
76 checks passed
nwparker added a commit that referenced this pull request Oct 29, 2026
A passive restore or remount reaches launch preparation too (#35813), so it no
longer runs as a user act; a stopped distro skips the install and the relay
installs once the distro runs. A skipped WSL home now returns not_installed
instead of falling through to the host writer, which would have written the
WSL home over UNC.
nwparker added a commit that referenced this pull request Oct 29, 2026
… distro

Every start main makes through the runtime controller is background work unless its caller names the user's act (CLI and window creates and splits, paired-client creates and tab opens, agent launches and sessions, conversation resumes). #35813's hold now also holds those fresh and restored starts while the distro is stopped or unverifiable.
nwparker added a commit that referenced this pull request Oct 29, 2026
…stopped distro (E9) (#35827)

* fix(wsl): Codex WSL hook installs and host hook checks never start a stopped distro (E9)

(a) installManagedHooksIntoWslRuntime checks the settled running gate inside
its trust queue right before the first UNC read, and again before the
post-hash fallback trust write; a later reconcile checks before revoking or
reinstalling. A stopped or unverifiable distro skips (null, as with no WSL
plan). The canonical-path probe runs through process-host behind
blockedWslDistroForExec; a refused probe settles as unavailable and keeps the
known identity. The guest shell preflight and a pane launch's home preparation
run as user acts on their distro.

(b) repo.hooksCheck takes an optional background flag (older hosts ignore it);
the renderer sends it, and the host refuses a stopped distro's background
check the same way the desktop's hooks:check does.

* fix(wsl): a pane launch is not a user act for the Codex hook install

A passive restore or remount reaches launch preparation too (#35813), so it no
longer runs as a user act; a stopped distro skips the install and the relay
installs once the distro runs. A skipped WSL home now returns not_installed
instead of falling through to the host writer, which would have written the
WSL home over UNC.

* fix(wsl): a fallback grant skipped mid-install reports trust pending

hooks.json is written before the hash wait, so a skip after it is not a clean
skip: return an error status saying the trust is pending instead of null.

* chore: drop the stale process-host direct-import baseline entry
nwparker added a commit that referenced this pull request Oct 29, 2026
… distro

Every start main makes through the runtime controller is background work unless its caller names the user's act (CLI and window creates and splits, paired-client creates and tab opens, agent launches and sessions, conversation resumes). #35813's hold now also holds those fresh and restored starts while the distro is stopped or unverifiable.
nwparker added a commit that referenced this pull request Oct 29, 2026
… distro (#35865)

* fix(wsl): main's background terminal starts never start a stopped WSL distro

Every start main makes through the runtime controller is background work unless its caller names the user's act (CLI and window creates and splits, paired-client creates and tab opens, agent launches and sessions, conversation resumes). #35813's hold now also holds those fresh and restored starts while the distro is stopped or unverifiable.

* fix(wsl): headless Run now starts as the user's act; update create expectations

A headless automation's Run now names the user's act through launchAgentTerminal, so it may start a stopped WSL distro; a scheduled run stays background and is held. The paired-client background create expectation now includes wslUserAct.

* fix(wsl): main's background start checks a folder workspace's path passively

With #35899's folder stat gate, the runtime preflight passes passiveWslRestore = !wslUserAct, so a background start never stats a stopped distro's folder as a user act before the daemon hold.
nwparker added a commit that referenced this pull request Oct 29, 2026
…tro (E9)

resolvePtySpawnHostEnv runs before #35813's hold. Every spawn but a passive
restore now runs its prep as a user act on its distro, so explicit launches
are unchanged. prepareForCodexLaunchAsync checks blockedWslDistroForExec once
for WSL targets and, when blocked, skips the guest-home probe, the legacy auth
drain, the config and instructions sync and the session bridge, returning the
home without probing. When not blocked, the home is warmed with getWslHomeAsync
so the sync reads hit the cache instead of a 5 s wsl.exe. The state-DB
recovery and the explicit plugin readiness wait are gated the same way.
nwparker added a commit that referenced this pull request Oct 29, 2026
…tro (E9) (#35858)

* fix(wsl): a passive restore's env prep never starts a stopped WSL distro (E9)

resolvePtySpawnHostEnv runs before #35813's hold. Every spawn but a passive
restore now runs its prep as a user act on its distro, so explicit launches
are unchanged. prepareForCodexLaunchAsync checks blockedWslDistroForExec once
for WSL targets and, when blocked, skips the guest-home probe, the legacy auth
drain, the config and instructions sync and the session bridge, returning the
home without probing. When not blocked, the home is warmed with getWslHomeAsync
so the sync reads hit the cache instead of a 5 s wsl.exe. The state-DB
recovery and the explicit plugin readiness wait are gated the same way.

* fix(wsl): only a launch someone asked for acts on its distro; gate the relay start

A passive restore or a non-WSL launch never runs as a user act, not even on
the default distro (a null distro resolved to the default before). The
daemon-off local provider applies the same rule, receiving wslPassiveRestore
from the spawn options. Starting the hook relay (ensureForDistro from every
WSL spawn's env build) now checks blockedForExec before the state check, so a
passive restore on a stopped distro starts no relay.
nwparker added a commit that referenced this pull request Oct 29, 2026
…#35927)

The renderer maps main's terminal_held_wsl_distro_stopped start failure to #35813's shared stopped card; its Start calls layout.startWslHeldPanes, which starts each pane through ensureRestoredPaneStarted as the user's act.
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