Skip to content

feat(wsl): let an idle WSL distro stop once its server idle-exits - #33084

Merged
nwparker merged 13 commits into
mainfrom
nwparker/wsl-holder-idle-release
Oct 27, 2026
Merged

nwparker merged 13 commits into
mainfrom
nwparker/wsl-holder-idle-release

Conversation

@nwparker

@nwparker nwparker commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​639 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​635
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​139 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​134

ELI5

When a WSL distro is added as an Orca host, Orca starts a small "holder" process that keeps the distro running, so terminals survive Orca quitting. Until now the holder never let go: the distro (and the whole WSL virtual machine, with its memory) stayed up forever, even when nothing was running in it. Now, once the Orca server inside the distro has been idle with no terminals, agents or automations, the holder steps aside and WSL can stop the distro like it does for any unused distro.

What Changed

Before: quit Orca with an empty WSL host and the distro stays Running indefinitely. The old WSL bridge let it stop.

After: the distro stops roughly 15 minutes (the server's existing idle timeout) plus a few seconds after the last Orca client leaves, as long as nothing is running in it. Anything live keeps it held exactly as before.

Mechanism (WSL hosts only, still behind ORCA_INTERNAL_WSL_HOSTS):

  • Server side (orcad): the server already idle-exits after 15 minutes with no client, no terminal, no working agent, no loaded chat provider, no enabled automation and no update in progress. When that idle stop also proved the terminal daemon empty and retired it, and the server runs inside WSL, it writes its own pid to ~/.orca/wsl-holder-release. The request is withdrawn if the stop turns out not to be idle (a signal or another stop took over) and whenever a new server starts.
  • Holder: its shell loop now checks for that file every 15 s. When it sees one, it waits for that server pid to exit, then removes the file with a plain rm (an atomic claim: if a newly started server withdrew it first, the holder keeps holding) and exits. The script stays POSIX/busybox-safe.
  • Desktop: after every connect, the liveness supervisor re-ensures the holder, which covers the narrow race where a holder claimed the previous server's request just before a reconnect. While connected, the existing 2-minute heartbeat already re-ensures it.
  • Startup: unchanged. E9 (fix(wsl-host): boot only the focused WSL host, and keep a stopped one down (E9 A) #33838) now owns how a stopped host reads.

"Idle" means nothing of the user's is running (review decision):

  • Before claiming a release, the holder scans /proc for live processes of its own user outside the holder itself.
  • A small allowlist of session plumbing is ignored (systemd (user instance), (sd-pam), dbus-daemon, see WSL_DISTRO_HOLDER_IGNORED_PROCESS_NAMES), as are zombies.
  • Anything else keeps the hold and is checked again on the next poll: a detached tmux, a nohup/setsid job, a dev server or docker started from an Orca terminal. It fails toward keeping the distro up.
  • Running containers also keep the hold: any containerd-shim* process, whoever owns it, because Docker's shims run as root. Root system services (dockerd with no containers, journald, cron) don't count.
  • Things that keep the hold, all on purpose (safe; the list stays tight):
    • on systemd distros, any other per-user service the login starts;
    • long-lived per-user agents (gpg-agent/keyboxd, ssh-agent or keychain from .bashrc, a lingering editor server);
    • everything, on a distro whose default user is root, because every system process then counts.
  • One exception to failing toward holding: if /proc is mounted hidepid, a root-owned container shim is invisible to the user, so it doesn't hold.
  • The scan reads each /proc/*/status only up to its Uid: line.

Other safety points:

  • Stale requests: a newly spawned holder removes any request left from before it started, so an older orcad's leftover can't end a holder a client just asked for.
  • Path: the holder hardcodes $HOME/.orca, while orcad uses the login shell's home (~/.orca from resolveOrcadMachineHome). If they ever differ, or HOME is unset, the holder never finds a request and just keeps holding, so it is safe.
  • orcad side: orcad touches the request file only for WSL-host launches (ORCA_WSL_ORCAD_HOST), and a failure to remove it is logged, never thrown, at start or stop.
  • Adopted holders (from a previous app run) have no exit listener. But every connect, and every liveness relaunch, re-ensures the holder after orcad is ready (arm). So a claim that races a new orcad start is caught then, not just by the 120 s heartbeat.

Why

  • Never kills live work. The distro is released only after the server proved there's nothing to lose: no terminal sessions (the daemon retired), nothing running. If the daemon's answer is unverifiable or live, it stays held. Losing contact is never treated as "nothing running".
  • Respects E9. Nothing here boots or reboots a distro. A stopped host waits until the user opens its work, as before.
  • Alternatives considered:
    • Release at desktop quit: this would miss work that finishes after quit, and quit would have to wait on a check over a bridge.
    • Have the server kill the holder by scanning processes: more invasive, and it would need process scanning inside the distro.
    • Reuse the existing idle-stop record: that file is written before the daemon's verdict is known, and it's discarded on cancel, so it can't tell the holder "safe to stop".

Wire compatibility: nothing new goes over RPC. A new desktop paired with an older server never sees a request, so it holds as today. An older holder ignores the file.

Builds on #34260 and #34307, both already on main. They stop the bash relay leaving a zombie and stop wsl.exe channels from being killed mid-command. Without them, a leftover WSL session kept the distro up.

Linked Issue

Internal WSL-via-orcad tracker R40 / E17.

Visual Proof

N/A. The only visible change is the existing "Distro stopped" row label showing at startup for a stopped distro. Behavior is proven by the real-WSL lane phase below.

Real-WSL result (head 3b7d5eb, which carried #34307 before it merged): the idle-release phase passed on ubuntu-24.04, debian-12 and fedora-41.

  • Each distro stopped 37–38 s after the last client.
  • The WSL VM process went from about 1.19–1.30 GB to gone.
  • While the distro was held, Orca's processes in it used about 260 MB.

Testing

  • Unit: orcad-wsl-holder-release.test.ts (writes only on a retired daemon inside WSL; not for live/unverifiable daemons, a returning client, outside WSL, or a taken-over stop; withdrawn at startup). wsl-distro-holder-script.test.ts runs the real script under /bin/sh: it holds with no request, waits for the pid, claims and exits 0, keeps holding if the request is withdrawn first, and ignores a garbage pid or a missing HOME. Supervisor and host-service tests cover the re-ensure and the startup status.

  • Script portability: ran the script in alpine (busybox ash), debian (dash) and ubuntu containers: it holds, waits for orcad, then exits 0 and removes the request.

  • Real WSL: new Phase 3 in wsl-orcad-e2e.yml. After the terminal closes, a detached setsid sleep must keep the holder live and its request unclaimed past the idle window, and killing it lets the distro stop. Phase 3 (wsl-orcad-real-distro-idle-release-lane.test.ts). A fresh server with a 15 s idle timeout and one live terminal keeps the distro Running past the idle and poll windows. After the terminal closes and the client leaves, the distro stops with no release call. It records the seconds to stop plus guest and VM memory before and after.

  • Automated tests added/updated

Notes

macOS/Linux are unaffected (no holder). SSH: the server writes the request only when WSL_DISTRO_NAME is set, and no SSH code reads it.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • N/A for visuals with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote impact considered
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (typecheck, oxlint and the changed-code gate pass locally; CI covers the rest)

@nwparker
nwparker marked this pull request as ready for review October 24, 2026 08:07
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

The changes add a WSL holder release-request file. Orcad writes it only when an idle stop is eligible and withdraws it when shutdown is discarded or a new Orcad starts. The holder polls every 15 seconds and exits after the recorded process exits and the request file is removed. WSL startup now reports confirmed-stopped distros, and the liveness supervisor ensures a holder when it arms a watch. An opt-in Windows test and workflow phase exercise idle release.

Priority: Normal

Merge Risk: Moderate · up to dc134

WSL lifecycle races can unexpectedly stop a replacement distro or prevent an idle distro from releasing, while the integration test may miss server restarts.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Description check Warning The description is detailed and covers the required change summary, rationale, testing, visual proof, notes, and checklist. However, the Linked Issue section does not provide an actual issue link or a… Add the actual linked issue reference in the Linked Issue section, such as Fixes #<issue-number>, or provide the repository-approved issue URL/reference for the internal tracker. Also complete or explicitly mark optional sections such as …
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: allowing an idle WSL distro to stop after the server idle-exits.
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: Description check

Explanation

The description is detailed and covers the required change summary, rationale, testing, visual proof, notes, and checklist. However, the Linked Issue section does not provide an actual issue link or a Fixes #... reference; it only lists “Internal WSL-via-orcad tracker R40 / E17.”

Resolution

Add the actual linked issue reference in the Linked Issue section, such as Fixes #&lt;issue-number&gt;, or provide the repository-approved issue URL/reference for the internal tracker. Also complete or explicitly mark optional sections such as AI Disclosure and Review if required by repository policy.

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.

@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: 2


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: 4c74efb3-59b8-4cd9-8499-b834d70b4fcb
Commits

Reviewing files that changed from the base of the PR and between 4c2a2ac and 426bd3b.

Files selected for processing: 14
  • .github/workflows/wsl-orcad-e2e.yml
  • src/main/orcad/orcad-managed-idle-exit-host.ts
  • src/main/orcad/orcad-wsl-holder-release.test.ts
  • src/main/orcad/orcad-wsl-holder-release.ts
  • src/main/wsl/wsl-distro-holder-command.ts
  • src/main/wsl/wsl-distro-holder-script.test.ts
  • src/main/wsl/wsl-distro-holder.test.ts
  • src/main/wsl/wsl-host-connector.ts
  • src/main/wsl/wsl-host-service.test.ts
  • src/main/wsl/wsl-host-service.ts
  • src/main/wsl/wsl-orcad-liveness-supervisor.test.ts
  • src/main/wsl/wsl-orcad-liveness-supervisor.ts
  • src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts
  • src/shared/orcad-idle-exit.ts

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

'p=; read -r p < "$f"',
"case $p in ''|*[!0-9]*) continue ;; esac",
'while kill -0 "$p" 2>/dev/null; do sleep 1; done',
'rm "$f" 2>/dev/null && exit 0',

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.

🩺 Stability & Availability | Major | 🏗️ Heavy lift

Do not remove a replacement release request.

If another Orcad replaces $f after this holder reads p, rm "$f" removes the new request and the holder exits without waiting for the new PID. A returning client could then have no holder keeping the distro running. Coordinate the claim with request publication, and exit only after confirming that the claimed request still belongs to p.


createManager()
connection = await manager.ensure(PLAIN)
expect(connection.pid, 'the same orcad served the reconnect').not.toBeNull()

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.

🎯 Functional Correctness | Minor | ⚡ Quick win

Compare the reconnected Orcad PID with the original PID.

A restarted Orcad has a non-null PID, so this assertion passes even when the original server did not survive the live-terminal interval. Save connection.pid before disposal, require it to be non-null, and compare it with the PID after reconnection.

@nwparker
nwparker force-pushed the nwparker/wsl-holder-idle-release branch from e0549f4 to e371036 Compare October 26, 2026 10:42

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Ensure the holder before the non-ready return. · wsl-orcad-liveness-supervisor.ts:144-151

src/main/wsl/wsl-orcad-liveness-supervisor.ts:144-151
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Ensure the holder before the non-ready return.

Deployment can adopt a holder that is still waiting for the previous Orcad to exit. That holder can exit before the new Orcad becomes ready. If attachWatch returns non-ready, this branch returns before ensureHolderQuietly. Subsequent unverifiable retries only schedule another verification. The live Orcad can therefore remain without a holder, so the distro cannot idle-release until another path re-ensures it.

Suggested fix
+    // A holder that took the previous orcad's idle release just before this launch is gone now.
+    await ensureHolderQuietly(this.deps.ensureHolder, entry.distro)
     if (!entry.watch && (await this.attachWatch(entry, connection)) !== 'ready') {
       if (entry.generation === generation) {
         this.scheduleVerify(entry)
       }
       return
     }
-    // A holder that took the previous orcad's idle release just before this launch is gone now.
-    await ensureHolderQuietly(this.deps.ensureHolder, entry.distro)

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: 665c8968-46a2-40e3-81d8-6174e7ed7d6f
Commits

Reviewing files that changed from the base of the PR and between e371036 and dc13407.

Files selected for processing: 1
  • src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts

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

@nwparker
nwparker force-pushed the nwparker/wsl-holder-idle-release branch 2 times, most recently from 70c1e5e to b63447f Compare October 27, 2026 06:08
The WSL host's holder kept the distro (and the WSL VM) running after Orca quit, even with
nothing running in it. orcad now leaves a release request in ~/.orca when a managed idle stop
retired its terminal daemon; the holder waits for that orcad to exit, claims the request and
ends, so WSL can stop the distro. Live terminals, working agents, enabled automations or a
returning client keep the distro held as before. A host whose distro is stopped at startup
reads as stopped instead of unreachable, and the real-WSL lane gains a phase that proves the
release and records memory.
…the lane phase

The real-WSL lane showed orcad idle-stopping with its daemon retired while the distro stayed
up: the setsid'd orcad's environment is not guaranteed to carry WSL_DISTRO_NAME, so the
release request was never written. Use ORCA_WSL_ORCAD_HOST, which only WSL-host launches set.
Phase 3 now carries its phase name and path filter, and records what kept a distro running.
Each guest command left its exec loop running, a wsl.exe client that kept the distro up after
the holder had already let go (the receipt showed no holder, no orcad, no request left).
@nwparker
nwparker force-pushed the nwparker/wsl-holder-idle-release branch from 3b7d5eb to 6c1de5b Compare October 27, 2026 07:52
…er trip on the request file

Review of #33084:
- The holder now checks, before claiming a release, that no process of the user runs outside
  the holder itself (session plumbing aside: systemd --user, (sd-pam), dbus-daemon). A detached
  tmux, nohup job or dev server keeps the distro; it fails toward holding.
- A new holder drops any request left from before it started, so an older orcad's request can
  never end a holder a client just asked for.
- orcad touches the request file only for WSL-host launches and never throws over it.
- The real-WSL lane proves a detached job keeps the holder and its request, and the distro
  stops once the job ends.
Docker's containerd-shim processes run as root, so the user-only scan missed them and a
release would have stopped running containers. Any containerd-shim process now keeps the hold.
…s the hold

Delta review of #33084: dash reads a status file a byte at a time, so stop at Uid. Document
that a root default user never releases, that per-user agents hold it, and the hidepid
exception. The lane's pkill no longer matches the shell running it.
@nwparker
nwparker merged commit ab9532e into main Oct 27, 2026
87 of 89 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