Skip to content

feat(wsl): liveness, failure classification and recovery for WSL orcad (WSL-via-orcad C6) - #27916

Merged
nwparker merged 3 commits into
mainfrom
nwparker/wsl-c6-liveness
Oct 13, 2026
Merged

nwparker merged 3 commits into
mainfrom
nwparker/wsl-c6-liveness

Conversation

@nwparker

@nwparker nwparker commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​787 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​781
Prod 13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1213 $\color{#cf222e}{\Huge{\mathbf{−}}}$​26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1187

Stacked on #27834 (C5: connecting to a WSL distro's orcad over wsl.exe stdio). Do not merge.

ELI5

After C5, the desktop can talk to the Orca server (orcad) inside a WSL distro. It could not yet tell when that server or its distro went away, or bring it back. Now it notices within about 150 ms, works out what happened, and either reconnects or reports that the terminals are gone.

It only reports terminals as gone when Windows can prove the distro stopped or restarted. A check that fails or can't be read is reported as "unverifiable", never as "exited". It also never starts a stopped distro for background work.

What Changed

  • Watch (wsl-orcad-watch.ts). A single bridge into orcad's port that stays open and never sends anything. When it ends, its exit code is the trigger: 0 means orcad closed the connection, 1 means the distro stopped. Opening a watch also works as a port probe, which answers READY or REFUSED.

  • Classifier (wsl-orcad-liveness-verdict.ts, pure). Exit codes from the bridge and the holder only start a check. They are never treated as proof. The verdict comes from:

    • a confirmed wsl --list --running read, via the new readConfirmedRunningWslDistros. Unlike the fail-closed list, a failed probe here comes back as unavailable;
    • the guest's process identity (wsl-guest-process-identity.ts): the boot id, pid 1's start ticks, and orcad's pid plus start ticks, all from /proc/<pid>/stat. None of these use wall-clock time.
    Evidence Verdict Terminals
    The list read failed unverifiable unverifiable
    Distro confirmed stopped distro-stopped exited
    Running, but the boot id or pid 1 start ticks changed distro-restarted exited
    Running, same incarnation, orcad pid gone or reused (or the port REFUSED when no pid is known) orcad-exited left to the census
    Same orcad process live n/a
    The guest or port did not answer unverifiable unverifiable

    The classifier never runs a command inside a distro that is not confirmed running, because that would boot it. After a failed exit, it waits 500 ms and reads the list again, because a distro that is still stopping can show as running.

  • Recovery (wsl-orcad-liveness-recovery.ts, wsl-orcad-liveness-supervisor.ts).

    • orcad crash or distro restart: relaunch through C4 on a bounded ladder of 250 ms, 1, 2, 4, 8, 15 and 30 s (with jitter), then report failed. Before reporting ready, it runs the orcad.terminalCensus RPC. While this runs, terminals stay unverifiable and are never marked exited.
    • Distro stopped: recover (which re-ensures the holder through C4) only if a user-visible WSL project or terminal needs it. Otherwise it reports distro-stopped and registers with the existing deferUntilWslDistroRunning gate.
    • orcad stopped with nothing visible: reports idle and does not relaunch, so orcad's managed 15-minute idle exit isn't undone.
    • A user-initiated connect takes over from any recovery in progress.
  • Timers. Heartbeats and backoff use performance.now(). The heartbeat runs every 120 s and only reads the running list. A heartbeat that fires more than twice its interval late triggers a full re-check. On power resume (subscribeSystemPowerLifecycle, which is fed by powerMonitor), it re-checks instead of trusting its timers.

  • Status (wsl-orcad-connection-status.ts). C5's onChange(distro, connection, status) now has a third argument, status. Its states are connecting, ready (with the census), reconnecting, distro-stopped, unverifiable, idle, failed, and closed. Every non-ready state carries a terminals verdict.

  • Holder (C3's open question). If the recorded holder stays unverifiable for 5 minutes (monotonic time) while its distro is confirmed running, ensureHeld spawns a replacement. A process table that has failed for that long isn't a hiccup, and a distro running all that time means something is holding it, so waiting is free. A list read that fails still never doubles the holder. Boot and release deadlines are now monotonic too. onHolderExit reports when a holder we spawned exits.

  • Census. requestOrcadTerminalCensus(connection, …) is extracted from the SSH census client, and the SSH path calls it.

  • Wiring. createWslOrcadLivenessSupervisor({ manager, holder, hasUserVisibleWork }) wires up the production probes. C7 supplies hasUserVisibleWork and the app wiring.

Why WSL can prove an exit when SSH cannot

An SSH host that goes quiet may still be running everything. WSL is different: the local WSL service answers wsl --list --running with a positive report. A distro it lists as stopped has no processes left, and a changed boot id or pid 1 start time means a new incarnation. These are answers, not silence. Everything else, including probe failure, timeouts and unreadable output, stays unverifiable.

Identity uses ticks since boot because the guest clock can jump after the host resumes (microsoft/WSL#4179, #10006).

Linked Issue

Internal WSL-via-orcad tracker, step C6.

Visual Proof

N/A: there is no UI here. C7 renders these statuses.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Real host (win-lowspec, WSL2). I used a throwaway Ubuntu 24.04 distro, orca-c6-probe, with systemd=false set before its first boot (pid 1 was init). The branch code was bundled and run under Orca.exe as Node, with the daily-build orcad template. Times are measured from when the harness spawned the trigger command.

  • (a) wsl --terminate:
    • The holder exited with code 1 at +113 ms and the watch with code 1 at +147 ms. The list read took 150 ms, and the verdict distro-stopped with terminals exited came at +296 ms.
    • With nothing visible, the distro stayed Stopped and the work was deferred.
    • With visible work, the second run took 321 ms to the verdict and 16.9 s to ready, including the distro boot and C4's relaunch.
  • (b) killing only orcad:
    • The watch exited with code 0 at +144 ms, the identity check showed the orcad pid gone, and the verdict orcad-exited came at +507 ms.
    • C4's relaunch took 12–13 s, the census 0.2 s, and the connection was ready at +13.7 to +14.8 s.
    • With a live WSL terminal created beforehand, the census reported liveSessions: 1: the terminal survived the crash and was never marked exited.
  • (c) list read blocked: I made the harness's running-list reader fail, then ran wsl --terminate.
    • It reported unverifiable / running-list-unavailable and kept re-checking at 1, 2 and 4 s.
    • There was no false exited for 12 s, even though the distro really was Stopped.
    • Once reads worked again, it gave the correct distro-stopped verdict and recovered.
    • I simulated the failure inside the harness rather than breaking wsl.exe itself.
  • Cleanup: I killed the holder, terminated and unregistered the distro, removed the temp dir and closed the terminal. wsl -l -v matches the starting state, and the other agents' distros weren't touched.

Automated: I added tests for every classifier branch, identity parsing, the backoff bounds and stable reset, the supervisor, the 5-minute holder bound, and the watch (against the real bridge script and the C5 fake wsl.exe children). The supervisor tests cover:

  • recovery with and without visible work, and that background work never boots the distro;
  • the crash path, where the census runs before ready;
  • idle, unverifiable and the give-up;
  • the 500 ms settle delay;
  • resume, ignoring the subscribe-time callback;
  • heartbeats that fire late;
  • wall-clock jumps;
  • stop, and duplicate stop reports.

Gates: pnpm tc, root oxlint, anti-slop, check:code-quality:changed, type-aware and native audits, the max-lines, ts-nocheck, focus-read and runtime-electron ratchets, reliability gates, dead classes, compat-boundary imports, process-host imports, the node pin, and pnpm format all pass. In src/main/{ssh,wsl,orcad,ipc}, 8835 tests pass. Two fail and also fail when run alone, in code this PR doesn't touch:

  • preflight-agent-detection (a known C4 item);
  • orcad-state-snapshot-shell, which is about real-shell process groups on macOS.

Two more passed when re-run alone.

AI Disclosure

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

Open questions:

  1. Recovery to ready takes 13–17 s, against the tracker's target of under 2 s. C4's relaunch (about 40 sequential wsl.exe calls) is almost all of that. Batching C4's wake path is the fix.
  2. Detection takes 150–300 ms against a target of under 100 ms. The exit itself arrives in about 145 ms, and the confirmed list read adds about 150 ms. Reporting before the list read would mean reporting without proof.
  3. A holder replaced after 5 minutes may leave the old one running if it was actually alive. Its pid is logged, but release only ends the recorded holder.

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)

@nwparker
nwparker changed the base branch from nwparker/wsl-c5-bridge-pairing to main October 13, 2026 13:14
@nwparker
nwparker marked this pull request as ready for review October 13, 2026 13:14
@nwparker
nwparker force-pushed the nwparker/wsl-c6-liveness branch from 7d5d2c4 to 4e4aa55 Compare October 13, 2026 13:14
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The changes add WSL Orcad liveness probes based on distro state, guest identity, and port signals. Connection updates now publish typed statuses and process IDs. Bridge watches report readiness and exits, and holder management uses monotonic deadlines with bounded replacement checks. A per-distro supervisor schedules verification, handles liveness outcomes, and coordinates reconnect recovery, census collection, and watch setup.

Estimated code review effort

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 4e4aa

The new WSL liveness supervision is mostly sound. Two small issues remain: status can briefly show ready when no connection exists, and short probes can keep extra WSL processes alive for a few seconds. The change is mergeable, and both issues are worth fixing.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 38.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 44 functions across 20 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 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.
Title check Passed The title clearly identifies the main change: WSL Orcad liveness monitoring, failure classification, and recovery. It is concise and related to the changeset.
Description check Passed The description is detailed and covers the required explanation, changes, rationale, testing, visual proof, checklist, and known limitations. The linked issue section names the internal C6 tracker but…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • 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
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a0a68e47-6eb6-4cc4-b3e4-07c01d2282dc
📥 Commits

Reviewing files that changed from the base of the PR and between b978bc4 and 4e4aa55.

📒 Files selected for processing (20)
  • src/main/ssh/orcad-terminal-census-client.ts
  • src/main/wsl-background-distro-gate.ts
  • src/main/wsl/wsl-distro-holder.test.ts
  • src/main/wsl/wsl-distro-holder.ts
  • src/main/wsl/wsl-guest-process-identity.test.ts
  • src/main/wsl/wsl-guest-process-identity.ts
  • src/main/wsl/wsl-orcad-connection-status.ts
  • src/main/wsl/wsl-orcad-connection.test.ts
  • src/main/wsl/wsl-orcad-connection.ts
  • src/main/wsl/wsl-orcad-liveness-model.ts
  • src/main/wsl/wsl-orcad-liveness-recovery.ts
  • src/main/wsl/wsl-orcad-liveness-supervisor.test.ts
  • src/main/wsl/wsl-orcad-liveness-supervisor.ts
  • src/main/wsl/wsl-orcad-liveness-verdict.test.ts
  • src/main/wsl/wsl-orcad-liveness-verdict.ts
  • src/main/wsl/wsl-orcad-liveness-wiring.ts
  • src/main/wsl/wsl-orcad-reconnect-backoff.test.ts
  • src/main/wsl/wsl-orcad-reconnect-backoff.ts
  • src/main/wsl/wsl-orcad-watch.test.ts
  • src/main/wsl/wsl-orcad-watch.ts

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

Comment on lines +244 to +250
const connection = this.deps.manager.get(entry.distro)
await gatherWslLivenessEvidence(evidence, {
readIdentity: () => this.deps.readIdentity(entry.distro, connection?.pid ?? null),
// Opening a watch is the port probe; a held one already proved the port.
probePort: async () =>
connection && !entry.watch ? this.attachWatch(entry, connection) : 'ready'
})

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

Do not report the port as ready when no connection exists.

probePort returns 'ready' in two cases. One is when a held watch exists, which is correct. The other is when connection is null, which is not. The port probe runs only when expected.orcad is null, so no pid identity exists to compare. If manager.get returns null in that state, classifyWslOrcadLiveness returns live. This can happen after the manager closes the connection, or while a port-move replacement is in progress (connection.ts Line 222). act then publishes ready and schedules a heartbeat, although nothing proved that orcad answers. The supervisor reports a connection that does not exist. Return 'unverifiable' when no connection exists. The retry loop then checks again.

🐛 Proposed fix
       // Opening a watch is the port probe; a held one already proved the port.
-      probePort: async () =>
-        connection && !entry.watch ? this.attachWatch(entry, connection) : 'ready'
+      probePort: async () => {
+        if (entry.watch) {
+          return 'ready'
+        }
+        return connection ? this.attachWatch(entry, connection) : 'unverifiable'
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const connection = this.deps.manager.get(entry.distro)
await gatherWslLivenessEvidence(evidence, {
readIdentity: () => this.deps.readIdentity(entry.distro, connection?.pid ?? null),
// Opening a watch is the port probe; a held one already proved the port.
probePort: async () =>
connection && !entry.watch ? this.attachWatch(entry, connection) : 'ready'
})
const connection = this.deps.manager.get(entry.distro)
await gatherWslLivenessEvidence(evidence, {
readIdentity: () => this.deps.readIdentity(entry.distro, connection?.pid ?? null),
// Opening a watch is the port probe; a held one already proved the port.
probePort: async () => {
if (entry.watch) {
return 'ready'
}
return connection ? this.attachWatch(entry, connection) : 'unverifiable'
}
})

Comment on lines +25 to +29
function transportFor(distro: string, environmentId = defaultWslOrcadEnvironmentId(distro)) {
return createWslOrcadHostTransport({
target: { kind: 'wsl', id: environmentId, distro, label: distro }
})
}

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 | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd -i 'wsl-command-loop' src | xargs -I{} sh -c 'echo "== {}"; cat -n {}' | head -200
rg -nP -C3 '\bcreateWslCommandLoopPool\s*\(' src
rg -nP -C3 'detectRemoteHostPlatform\s*\(' src/main/ssh | head -40

Repository: stablyai/orca

Length of output: 12583


🏁 Script executed:

set -o pipefail
printf '%s\n' '== wiring =='
nl -ba src/main/wsl/wsl-orcad-liveness-wiring.ts
printf '%s\n' '== host transport =='
nl -ba src/main/wsl/wsl-orcad-host-transport.ts
printf '%s\n' '== liveness references =='
rg -n -F --glob '*.ts' -e 'transportFor(' -e 'readIdentity' -e 'openWatch' -e 'watch.close' src/main/wsl src/main/ssh
printf '%s\n' '== watch implementation =='
rg -n -F --glob '*.ts' -e 'createWslOrcadConnectionWatch' -e 'openWslOrcadConnectionWatch' -e 'ConnectionWatch' src/main/wsl src/main/ssh
printf '%s\n' '== changed-file diff =='
git diff --no-ext-diff --unified=40 b978bc4dc266432b12bd06bb0d69d77308093597 4e4aa55f7d65d50468b737208805617f6815b8d4 -- src/main/wsl/wsl-orcad-liveness-wiring.ts

Repository: stablyai/orca

Length of output: 15053


🏁 Script executed:

set -o pipefail
nl -ba src/main/wsl/wsl-orcad-liveness-wiring.ts
nl -ba src/main/wsl/wsl-orcad-host-transport.ts
rg -n -C 8 -F --glob '*.ts' -e 'transportFor(' -e 'openWatch(' -e 'readIdentity(' -e 'watch.close' src/main/wsl src/main/ssh
rg -n -C 12 -F --glob '*.ts' -e 'createWslOrcadConnectionWatch' -e 'openWslOrcadConnectionWatch' -e 'ConnectionWatch' src/main/wsl src/main/ssh

Repository: stablyai/orca

Length of output: 23536


🏁 Script executed:

set -o pipefail
printf '%s\n' '== WSL watch =='
nl -ba src/main/wsl/wsl-orcad-watch.ts
printf '%s\n' '== bridge entrypoints =='
rg -n -C 12 -F --glob '*.ts' -e 'openOrcadStdioBridgeChannel' -e 'resolveOrcadStdioBridge' -e 'watchChannel' src/main/ssh src/main/wsl
printf '%s\n' '== bridge implementations =='
rg -n --files src/main/ssh src/main/wsl | rg '(stdio|watch|channel)'

Repository: stablyai/orca

Length of output: 30781


🏁 Script executed:

set -o pipefail
printf '%s\n' '== guest identity =='
nl -ba src/main/wsl/wsl-guest-process-identity.ts
printf '%s\n' '== execCommand =='
rg -n -C 12 -F --glob '*.ts' -e 'export async function execCommand' -e 'function execCommand' -e 'execWithClosedStdin' src/main/ssh/ssh-relay-exec-command.ts src/main/ssh
printf '%s\n' '== platform detection =='
nl -ba src/main/ssh/ssh-remote-platform-detection.ts

Repository: stablyai/orca

Length of output: 27440


Disable pooled command loops for short-lived liveness probes.

transportFor creates a new pooled transport for each identity read and watch setup. A completed probe can leave its wsl.exe command loop alive for up to 5 seconds. Separate transports can retain overlapping loops.

The watch's long-lived bridge uses transport.exec and is closed by watch.close(). The issue is the idle probe loop, not the bridge lifetime.

🔧 Suggested fix
--- "a/src/main/wsl/wsl-orcad-liveness-wiring.ts"
+++ "b/src/main/wsl/wsl-orcad-liveness-wiring.ts"
@@ -22,11 +22,12 @@
   daemonProtocolVersion: null
 }
 
 function transportFor(distro: string, environmentId = defaultWslOrcadEnvironmentId(distro)) {
   return createWslOrcadHostTransport({
-    target: { kind: 'wsl', id: environmentId, distro, label: distro }
+    target: { kind: 'wsl', id: environmentId, distro, label: distro },
+    commandLoops: false
   })
 }
 
 /** Every session counts (activation time 0): liveness asks how many survived, not how new. */
 function collectWslOrcadCensus(connection: WslOrcadConnection): Promise<OrcadTerminalCensus> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function transportFor(distro: string, environmentId = defaultWslOrcadEnvironmentId(distro)) {
return createWslOrcadHostTransport({
target: { kind: 'wsl', id: environmentId, distro, label: distro }
})
}
function transportFor(distro: string, environmentId = defaultWslOrcadEnvironmentId(distro)) {
return createWslOrcadHostTransport({
target: { kind: 'wsl', id: environmentId, distro, label: distro },
commandLoops: false
})
}

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