Skip to content

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

Merged
nwparker merged 2 commits into
mainfrom
nwparker/wsl-env-prep-gate
Oct 29, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/wsl-env-prep-gate

Conversation

@nwparker

@nwparker nwparker commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​459 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​441
Prod 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​194 $\color{#cf222e}{\Huge{\mathbf{−}}}$​98 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​96

ELI5

#35813 stops a terminal that Orca restores by itself from starting a stopped WSL Linux. But before that check runs, Orca prepares the terminal's environment, and for Codex that preparation already reached into the distro. It looked up the Linux home folder, copied config files, ran WSL scripts and started a history bridge. Any of those started the distro before #35813 could hold the pane. Now a restore nobody asked for does none of that on a stopped distro. Opening a terminal yourself, or pressing Start, still does all of it.

What Changed

Before: resolvePtySpawnHostEnv runs for every WSL PTY spawn that isn't preAdopted, including passive restores, before #35813's DaemonPtyAdapter hold. In it:

  • prepareForCodexLaunchAsync (WSL targets):
    • getWslHome, a synchronous wsl.exe probe that blocks main for up to 5 s, run whenever the home isn't cached;
    • the legacy auth drain (runWslProcess / readWslCodexAuths, which is wsl.exe);
    • the config and instructions sync (mkdirSync and copies on \\wsl.localhost);
    • the WSL session bridge (wsl.exe).
  • ensureCodexStateDbBackfillRecoveryStarted, which reads the home's state DB over UNC.
  • awaitExplicitAgentPluginGuestReadiness, which ensures the hook relay (wsl.exe) for pi, omp and Claude launches.

Each of these started a stopped distro before the hold could act.

After:

  • Spawn path: a WSL launch someone asked for runs its env prep as a user act on its distro, via runAsWslLaunchUserAct. If no distro is named, that means the default distro. A passive restore (wslPassiveRestore, from fix(wsl): a passive restore never starts a stopped WSL distro; its bridge panes wait with Start #35813) and a non-WSL launch never run as a user act, not even on the default distro, so their prep goes through the E9 gates. This applies in both places env prep happens:
    • resolvePtySpawnHostEnv (the daemon and runtime path);
    • the daemon-off local provider's buildSpawnEnv, which now receives wslPassiveRestore from the spawn options.
  • prepareForCodexLaunchAsync: for a WSL target it checks blockedWslDistroForExec once. When the distro is blocked (stopped or unverifiable), it skips the probe, drain, sync and bridge, and returns the home without probing:
    • an account home comes from settings and the recorded identity;
    • system default comes from the cached guest home, or null when uncached. null means the guest's own ~/.codex, which is the system default, so the identity is still correct.
  • The 5 s sync probe: when not blocked, the guest home is warmed with getWslHomeAsync first, so the synchronous getWslHome reads after it hit the cache instead of blocking main.
  • State-DB recovery: it starts only if blockedWslDistroForPath(home) passes.
  • Plugin readiness: it returns false (not ready) without ensuring the relay when blockedWslDistroForExec(distro) blocks.
  • Hook relay start: every WSL spawn with agent hooks on calls wslHookRelayManager.ensureForDistro from buildPtyHostEnv, and starting a relay is a wsl.exe launch. ensureInternal now checks blockedForExec before its state check, so concurrent ensures still join one start, and a blocked distro gets no relay. Explicit launches pass the check through the user act.
  • Claude and OMP env prep: checked. For WSL, the OMP prep only copies env keys, and Claude's readyForLaunch and the Antigravity prep both return early; none of them touch UNC. The relay ensure above is the only WSL touch on that path.
  • Codex resume prep: traced; no change needed. The desktop's prepareCodexSessionResumeForLaunch returns null for WSL targets before any I/O. The orcad variant runs inside the distro.

Why

A held restore never spawns, and the user's Start re-runs the launch as a user act, which does the skipped work then. So the gate only removes work that would have booted the distro for a pane that #35813 holds anyway.

Gating at a single entry (prepareForCodexLaunchAsync), rather than inside each of getWslHome, the drain, the sync and the bridge, keeps those helpers unchanged for their other callers.

The user-act scope also covers the Codex hook install's follow-up from #35827: a genuine user launch now runs prepareRuntimeHomeForLaunch inside the act, so its hooks install before spawn. A passive restore still doesn't.

Linked Issue

Internal: E9, the env-prep booter in front of #35813's hold. Sequenced before #35733.

Visual Proof

N/A: no UI change; #35813's card shows as before.

Testing

  • runtime-home-wsl-passive-restore-prep.test.ts, using the real rule (blocked unless the distro is the current user act):
    • a passive restore gets the account home with no getWslHome/getWslHomeAsync, drain, sync or bridge call;
    • system default with an uncached home returns null without probing;
    • the same launch as a user act warms the home asynchronously and drains.
  • spawn-host-env-wsl-passive-restore.test.ts, with listWslDistrosAsync mocked to ['Ubuntu']:
    • an explicit launch runs as a user act on its distro, and a passive restore doesn't;
    • a passive restore with no named distro is never a user act on the default;
    • a host-target spawn never is either;
    • an explicit WSL launch with no named distro acts on the default;
    • a stopped distro skips the state-DB recovery, while a running one still starts it.
  • wsl-agent-plugin-readiness.test.ts: a stopped distro's relay isn't ensured, and a running one is.
  • wsl-background-exec-gate.test.ts: runAsWslLaunchUserAct acts on the named or default distro only for an asked-for WSL launch, never for a passive restore or a non-WSL launch.
  • wsl-hook-relay-manager.test.ts: a blocked distro gets no relay.
  • local-pty-pi-owner-env.test.ts: the daemon-off env builder receives wslPassiveRestore.
  • Existing WSL account suites now mock the exec gate as passing (their launches are user acts) and the async home probe. The drain-ordering test waits for the drain instead of assuming one microtask.
  • pnpm run typecheck:ci, oxlint, oxfmt and check:code-quality:changed pass. codex-accounts, ipc/pty, daemon, agent-hooks, codex, startup and providers pass (10,575 before the rebase; 5,095 in the touched suites after).

Review

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.

Notes

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

The changes add a WSL launch-user gate and pass passive-restore status through PTY environment preparation. They prevent Codex home probing and selected background preparation when WSL execution is blocked, and skip state DB recovery for blocked homes. Agent-plugin readiness and relay setup now check whether a distro is blocked before proceeding. Tests cover these paths and add asynchronous WSL home mocks to existing tests.

Priority: Normal

Merge Risk: Moderate · up to 2f78c

Starting an agent through a paired WSL environment can skip Codex authentication and resource preparation when the distro is stopped. Fix that launch path before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title names the main change: a passive restore's environment preparation no longer starts a stopped WSL distro. The changeset gates Codex launch prep, spawn env prep, state-DB recovery, plugin rea…
Description check Passed The description follows the template closely. It has ELI5, What Changed (before/after with mechanism), Why (including alternatives considered), Linked Issue, Visual Proof marked N/A with a reason, Tes…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: a passive restore's environment preparation no longer starts a stopped WSL distro. The changeset gates Codex launch prep, spawn env prep, state-DB recovery, plugin readiness, and hook-relay startup behind a blocked-distro check and a user-act scope for passive restores. The title is specific, follows the conventional commit format, and names the ticket reference (E9). It omits the gating mechanism, which is acceptable for a title.

Full details: Description check

Explanation

The description follows the template closely. It has ELI5, What Changed (before/after with mechanism), Why (including alternatives considered), Linked Issue, Visual Proof marked N/A with a reason, Testing with checkboxes and specific test files, Review, Agent skill upstream boundary, and Notes. The Linked Issue section names an internal ticket (E9) and a sequencing dependency rather than a GitHub issue. The template requires a linked issue for outside contributors, and the author is presumably an internal contributor, so this is a minor gap. The AI Disclosure section was omitted, which the template allows for internal team members. The Checklist section is not filled in. The description is mostly complete, so the check passes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 9 functions across 21 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • 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: 4/5

[Medium impact] The changes affect WSL terminal setup and agent hooks, not host launches.

The PR still needs a fix for the hooks-disabled Codex path, which can touch a stopped distro during passive prep.

Findings

  1. P1 Hook prep starts stopped distro ▶
  2. P2 Relay starts after hooks stop ▶
  3. P2 Failed warm-up still blocks desktop ▶

Summary

Stops passive WSL environment prep from starting a stopped distro while keeping explicit launches inside a user-act scope.

  • Requested WSL launches prepare their terminal inside a user-act scope.
  • Codex preparation skips guest work for a stopped WSL distro.
  • WSL agent-hook setup waits for a running distro before starting its relay.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[WSL environment prep] --> B{Explicit launch?}
  B -->|Yes| C[Run inside user act]
  B -->|No| D[Check distro state]
  D -->|Stopped or unknown| E[Skip guest prep]
  D -->|Running| F[Prepare guest environment]
  C --> F
  E --> G[Existing restore hold]
Loading

Reviews (2) · Last reviewed commit: "fix(wsl): only a launch someone asked fo..." · Reviewed by Greptile

Comment thread src/main/ipc/pty/spawn-host-env.ts Resolved
Comment on lines +141 to +142
if (await blockedWslDistroForExec(wslTarget.wslDistro)) {
return this.getWslCodexHomePathForSelection(wslTarget, { probeHome: false })

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 Hook prep starts stopped distro

Returning a recorded account home here does not stop the caller's hook prep. createCodexRuntimeHomeLaunchPreparation passes that home to prepareRuntimeHomeForLaunch, which starts a wsl.exe path probe and reads the guest's hooks.json. A passive restore of a managed account on a stopped non-default distro therefore still starts it, even when this gate blocks correctly.

Carry the blocked result through to the caller and skip guest hook prep too.

Comment on lines +145 to 149
if (distro && process.platform === 'win32') {
// Warm the guest-home cache off the main thread, so the sync reads below skip a 5 s wsl.exe.
await getWslHomeAsync(distro)
}
const homePath = this.getWslCodexHomePathForSelection(wslTarget)

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 Failed warm-up still blocks desktop

If the async home probe fails, getWslHomeAsync returns null without caching it and clears its in-flight entry. The following home resolution and auth drain can then call getWslHome, which runs another blocking five-second probe. This leaves the desktop freeze that the warm-up is meant to remove.

Keep an unknown home from falling back to synchronous guest probes during this async launch path, and cover a failed warm-up in the tests.

Knowledge Base Used: Stop WSL watchers from killing cold distro boots

…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.
…e 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
nwparker force-pushed the nwparker/wsl-env-prep-gate branch from 84a4ee8 to 2f78cd5 Compare October 29, 2026 11:55
Comment on lines +131 to +133
if ((await this.deps.blockedForExec(distro)) || this.disposed) {
return
}

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 Relay starts after hooks stop

If the user turns hooks off while blockedForExec is waiting, disposeAll({ permanent: false }) clears the relays but leaves this.disposed false. This continuation then starts a new relay without checking the hooks setting again. That relay escapes the disable cleanup and can keep WSL running.

Recheck isWslHookRelayAllowed(this.deps) after the wait, before creating state or starting a relay.

Suggested change
if ((await this.deps.blockedForExec(distro)) || this.disposed) {
return
}
if (
(await this.deps.blockedForExec(distro)) ||
this.disposed ||
!isWslHookRelayAllowed(this.deps)
) {
return
}

@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
  • 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: 7324142c-53e1-41a2-a649-23d3b9709e52
Commits

Reviewing files that changed from the base of the PR and between 43ed987 and 2f78cd5.

Files selected for processing: 21
  • src/main/agent-hooks/wsl-agent-plugin-readiness.test.ts
  • src/main/agent-hooks/wsl-agent-plugin-readiness.ts
  • src/main/agent-hooks/wsl-hook-relay-deps.ts
  • src/main/agent-hooks/wsl-hook-relay-manager.test.ts
  • src/main/agent-hooks/wsl-hook-relay-manager.ts
  • src/main/codex-accounts/runtime-home-service-home-routing.ts
  • src/main/codex-accounts/runtime-home-service-launch.ts
  • src/main/codex-accounts/runtime-home-service-state.ts
  • src/main/codex-accounts/runtime-home-service-wsl.ts
  • src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts
  • src/main/codex-accounts/runtime-home-wsl-passive-restore-prep.test.ts
  • src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts
  • src/main/ipc/pty/ipc/spawn-env-codex.ts
  • src/main/ipc/pty/provider/local-configure.ts
  • src/main/ipc/pty/spawn-host-env-wsl-passive-restore.test.ts
  • src/main/ipc/pty/spawn-host-env.ts
  • src/main/providers/local-pty-pi-owner-env.test.ts
  • src/main/providers/local-pty-provider-types.ts
  • src/main/providers/local-pty-spawn-environment.ts
  • src/main/wsl/wsl-background-exec-gate.test.ts
  • src/main/wsl/wsl-background-exec-gate.ts

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

Comment on lines +141 to +143
if (await blockedWslDistroForExec(wslTarget.wslDistro)) {
return this.getWslCodexHomePathForSelection(wslTarget, { probeHome: false })
}

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.

Minor · Functional Correctness · Quick win

Supported by static analysis

Script executed:

#!/bin/bash
# Callers of the async Codex launch preparation and the selected-home getter.
rg -nP --type=ts -g '!**/*.test.ts' '\bprepareForCodexLaunchAsync\s*\(' src
rg -nP --type=ts -g '!**/*.test.ts' '\bgetSelectedCodexHomePath\b' src
# Callers of the user-act wrappers, for comparison.
rg -nP --type=ts -g '!**/*.test.ts' '\brunAsWsl(Launch|Exec)?UserAct\b' src

Repository: stablyai/orca

Length of output: 6489


Script executed:

set -eu
printf '%s\n' '--- runtime-home-service-launch.ts ---'
nl -ba src/main/codex-accounts/runtime-home-service-launch.ts | sed -n '1,210p'
printf '%s\n' '--- codex-runtime-home-launch-preparation.ts ---'
nl -ba src/main/codex/codex-runtime-home-launch-preparation.ts | sed -n '1,120p'
printf '%s\n' '--- wsl-orcad-account-env-wiring.ts ---'
nl -ba src/main/startup/wsl-orcad-account-env-wiring.ts | sed -n '1,120p'
printf '%s\n' '--- spawn-host-env.ts ---'
nl -ba src/main/ipc/pty/spawn-host-env.ts | sed -n '1,210p'
printf '%s\n' '--- local-configure.ts ---'
nl -ba src/main/ipc/pty/provider/local-configure.ts | sed -n '1,190p'
printf '%s\n' '--- wsl-background-exec-gate.ts ---'
nl -ba src/main/wsl/wsl-background-exec-gate.ts | sed -n '1,120p'
printf '%s\n' '--- related definitions and bindings ---'
rg -n -F --glob '*.ts' -e 'getSelectedCodexHomePath =' -e 'getSelectedCodexHomePath:' -e 'createGetSelectedCodexHomePath' -e 'prepareForCodexLaunchAsync(' src || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 41302


Script executed:

set -eu
printf '%s\n' '--- installWslOrcadAccountEnvDependencies definitions and callers ---'
rg -n -F --glob '*.ts' -e 'installWslOrcadAccountEnvDependencies' -e 'installWslOrcadAccountShim' src
printf '%s\n' '--- wsl-orcad-account-env.ts ---'
nl -ba src/main/wsl/wsl-orcad-account-env.ts | sed -n '1,280p'
printf '%s\n' '--- call-site context ---'
for f in $(rg -l -F --glob '*.ts' -e 'installWslOrcadAccountShim(' src); do
  echo "--- $f ---"
  nl -ba "$f" | sed -n '1,180p'
done

Repository: stablyai/orca

Length of output: 24970


Script executed:

set -eu
printf '%s\n' '--- withWslOrcadAccountEnv callers ---'
rg -n -F --glob '*.ts' -e 'withWslOrcadAccountEnv(' -e 'getWslOrcadAccountEnvDependencies' src
printf '%s\n' '--- surrounding callers ---'
for f in $(rg -l -F --glob '*.ts' 'withWslOrcadAccountEnv(' src); do
  echo "--- $f ---"
  nl -ba "$f" | sed -n '1,260p'
done

Repository: stablyai/orca

Length of output: 35301


Run the WSL account-environment preparation inside a user-act scope.

The runtime-environment call handler applies the account-environment shim to explicit launch requests, but it calls prepareForCodexLaunchAsync without a user-act scope. For a stopped WSL distro, the new guard therefore skips Codex authentication draining, resource sync, and session-bridge preparation for a user-triggered launch.

Wrap this boundary for WSL environments and preserve the existing unwrapped path for non-WSL environments.

🐛 Suggested fix
+import { runAsWslExecUserAct } from '../../wsl/wsl-background-exec-gate'
 import { withWslOrcadAccountEnv } from '../../wsl/wsl-orcad-account-env'
...
-      const accounts = await withWslOrcadAccountEnv({
-        environmentId: environment.id,
-        method: args.method,
-        params: args.params,
-        hostSupports: (capability) =>
-          wslOrcadHostSupports(environment.id, capability, () =>
-            callRuntimeEnvironment(getUserDataPath(), environment.id, 'status.get', undefined)
-          )
-      })
+      const prepareAccounts = () =>
+        withWslOrcadAccountEnv({
+          environmentId: environment.id,
+          method: args.method,
+          params: args.params,
+          hostSupports: (capability) =>
+            wslOrcadHostSupports(environment.id, capability, () =>
+              callRuntimeEnvironment(getUserDataPath(), environment.id, 'status.get', undefined)
+            )
+        })
+      const wslDistro = wslOrcadHostDistro(environment.id)
+      const accounts = await (wslDistro
+        ? runAsWslExecUserAct(wslDistro, prepareAccounts)
+        : prepareAccounts())

@nwparker
nwparker merged commit 434ffba into main Oct 29, 2026
60 checks passed
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