Repository navigation
fix(wsl): scan agent sessions only in running distros - #17072
Conversation
Greptile SummaryThis PR limits WSL session discovery and transcript access to running distributions so history refreshes do not start stopped WSL instances.
Confidence Score: 4/5The PR is not yet safe to merge because a failed WSL hook path can still prevent native chat from finding an existing Codex transcript by session ID. The resolver returns or throws immediately after failed WSL hook-path resolution, even though its existing Codex ID resolver can search other host and running-WSL roots for the same session. Files Needing Attention: src/main/native-chat/session-file-resolver.ts
|
| Filename | Overview |
|---|---|
| src/main/native-chat/session-file-resolver.ts | Shares WSL snapshots across transcript resolution, but still short-circuits Codex ID fallback after a failed WSL hook path. |
| src/main/native-chat/host-readable-transcript-path.ts | Adds running-distro snapshots and gates UNC, guest-path, and managed Codex-root resolution without making exact UNC checks await HOME probes. |
| src/main/native-chat/transcript-watch-engine.ts | Replaces native WSL watching and reconciliation with running-state-driven polling while preserving non-WSL watcher behavior. |
| src/main/wsl-running-distro-cache.ts | Centralizes and coalesces running-distribution discovery for concurrent WSL consumers. |
| src/main/ai-vault/cached-session-list.ts | Restricts WSL and configured Codex scan roots to distributions reported as running. |
Sequence Diagram
sequenceDiagram
participant C as Native chat
participant R as Session resolver
participant W as WSL running-state discovery
participant F as Transcript filesystem
C->>R: Resolve hook path and session ID
R->>W: List running distributions
W-->>R: Shared running snapshot
R->>F: Probe path only in running distribution
alt Hook path readable
F-->>R: Transcript path
R-->>C: Install polling subscription
else Hook path unavailable
F-->>R: Missing or refusal
R-->>C: Missing or unavailable result
end
Reviews (7): Last reviewed commit: "fix(build): list the new WSL cache modul..." | Re-trigger Greptile
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds shared asynchronous discovery for running WSL distros and their home directories. WSL paths are filtered against active distros. AI Vault scanning uses active WSL homes. Transcript resolution shares runtime state across each resolve attempt. Transcript watchers observe distro state, guard reads and installs, and revalidate paths after distro changes. Tests cover discovery, filtering, observer behavior, snapshot handling, and watcher lifecycle. Merge Risk: 🟡 Moderate · up to The change prevents most WSL scans from starting stopped distros, but the running-state check is only a snapshot: if a distro stops between discovery and a later home, path, or watcher operation, that operation can still restart it. This creates a bounded lifecycle-control and resource-use risk that should be explicitly accepted or fixed before merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The PR addresses Full details: Out of Scope Changes checkExplanation The changes remain within the stated WSL session-discovery objective. Updates to native-chat resolution, transcript watchers, path filtering, caching, and the hook relay support running-distro validation described in the PR objectives. No unrelated UI, IPC, remote-wire, or settings changes are included. Full details: Description checkExplanation The description is complete and follows the repository template. It explains the change and motivation, links issue ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 550364c5-5478-4b01-9d3b-79a44b29db9e
📒 Files selected for processing (17)
src/main/agent-hooks/wsl-hook-relay-launch.tssrc/main/ai-vault/cached-session-list.test.tssrc/main/ai-vault/cached-session-list.tssrc/main/ipc/ai-vault-scan-coalescing.test.tssrc/main/ipc/ai-vault.test.tssrc/main/native-chat/host-readable-transcript-path.test.tssrc/main/native-chat/host-readable-transcript-path.tssrc/main/native-chat/session-file-resolver-codex-roots.test.tssrc/main/native-chat/session-file-resolver-wsl-scan-gate.test.tssrc/main/native-chat/session-file-resolver-wsl.test.tssrc/main/native-chat/session-file-resolver.tssrc/main/native-chat/transcript-watch-resolve-poll.test.tssrc/main/native-chat/transcript-watch-wsl-exact-path.test.tssrc/main/native-chat/transcript-watch.tssrc/main/wsl-running-distros.test.tssrc/main/wsl-running-path-filter.tssrc/main/wsl.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| const probe = execFileUtf8('wsl.exe', ['-d', distro, '--exec', 'bash', '-c', 'echo $HOME']) | ||
| .then((output) => { | ||
| const home = output.trim() | ||
| if (!home || !home.startsWith('/')) { | ||
| return null | ||
| } | ||
| const uncPath = toWindowsWslPath(home, distro) | ||
| wslHomeCache.set(distro, uncPath) | ||
| return uncPath | ||
| }) | ||
| .catch(() => null) | ||
| .finally(() => { | ||
| if (wslHomeProbeCache.get(distro) === probe) { | ||
| wslHomeProbeCache.delete(distro) | ||
| } | ||
| }) | ||
| wslHomeProbeCache.set(distro, probe) | ||
| return probe | ||
| } | ||
|
|
||
| const uncPath = toWindowsWslPath(home, distro) | ||
| wslHomeCache.set(distro, uncPath) | ||
| return uncPath | ||
| } catch { | ||
| return null | ||
| } | ||
| /** UNC home roots for distros that are running at discovery time. */ | ||
| export async function listRunningWslHomeDirsAsync(): Promise<string[]> { | ||
| const homes = await Promise.all( | ||
| (await listRunningWslDistrosAsync()).map((distro) => getWslHomeAsync(distro)) | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Prevent the HOME probe from restarting a distro that stopped after discovery.
Line 348 invokes getWslHomeAsync after the running-list snapshot completes. If that distro stops in this interval, Line 325 runs wsl.exe -d <distro>, which starts it again. This reintroduces the stopped-distro side effect during vault scans and transcript snapshots.
Use a HOME discovery mechanism that cannot start WSL. A second running-list check does not close this TOCTOU gap. Add a regression test that terminates the distro after listing and before the queued HOME probe.
🧰 Tools
🪛 ast-grep (0.45.2)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile, execFileSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b9632b9-2019-4b1e-a75a-8d1581336b52
📒 Files selected for processing (16)
src/main/native-chat/host-readable-transcript-path.tssrc/main/native-chat/session-file-resolver-codex-roots.test.tssrc/main/native-chat/session-file-resolver-wsl-scan-gate.test.tssrc/main/native-chat/session-file-resolver.tssrc/main/native-chat/transcript-incremental-reader.tssrc/main/native-chat/transcript-watch-engine-wsl-lifecycle.test.tssrc/main/native-chat/transcript-watch-engine.tssrc/main/native-chat/transcript-watch-resolve-poll.test.tssrc/main/native-chat/transcript-watch-wsl-exact-path.test.tssrc/main/native-chat/transcript-watch-wsl-stall.test.tssrc/main/native-chat/transcript-watch.tssrc/main/native-chat/transcript-watcher-count.tssrc/main/native-chat/transcript-watcher-install-probe.tssrc/main/native-chat/wsl-transcript-running-observer.test.tssrc/main/native-chat/wsl-transcript-running-observer.tssrc/main/native-chat/wsl-transcript-watcher-running-guard.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| if (needsWslHostResolution(hookPath)) { | ||
| if (unavailable) { | ||
| throw unavailable | ||
| } | ||
| return null | ||
| } |
There was a problem hiding this comment.
…discovery listRunningWslDistrosAsync failed closed on any probe error (timeout, ENOENT, wsl.exe hiccup), indistinguishable from "no distros running". A 2s poll (wsl-transcript-running-observer.ts) calls it indefinitely while any WSL transcript tab is open, so a persistently broken wsl.exe silently made every WSL session vanish app-wide with no way to tell "discovery broken" from "distro stopped", and re-spawned wsl.exe every 2s forever. Extract a dedicated cache/backoff module (wsl-running-distro-cache.ts, mirroring the sibling machinery already in wsl.ts for the full distro list) so a probe failure falls back to the last-known-good running-distro list and backs off further probes, while a genuine empty result (no distros running) stays authoritative. Add a consumer-level test simulating a sustained wsl.exe outage across a live transcript-watcher polling session, asserting the observer keeps reporting "running" and that real wsl.exe spawns stay bounded.
A WSL probe that cannot reach its distro returns the same value as one that asked and got "no". Downstream nothing can tell them apart, so a distro that was busy for a second reports no git, or no agent sessions, until relaunch — sticky, silent, and identical to the real thing. That has shipped three times: preflight CLI probes, the glab auth fallback (#8941), and listRunningWslDistrosAsync failing closed with no last-known-good while polled every 2s (PR #17072). Scan the WSL and preflight probe modules for the shape and hold the current set in an allowlist that only shrinks. Scoped deliberately: the same shape appears ~850 times across src/ and is usually correct, because for most callers a failure really does mean absent. It is only dangerous where the answer describes a distro. The guard cannot see the dangerous part — whether the value is later cached or gates discovery is dataflow, not syntax. It stops a new swallow site appearing here without someone saying why it is safe to pin, which is the review that was missing all three times.
config/tsconfig.tc.web.json enumerates its files explicitly, so a new module imported by wsl.ts fails the full typecheck with TS6307 until it is listed. pnpm tc:node passes without it, which is how this got missed. src/main/wsl.ts(13,8): error TS6307: File 'src/main/wsl-running-distro-cache.ts' is not listed within the file list of project 'config/tsconfig.tc.web.json'.
) * test(wsl): guard probes that report failure as a negative answer A WSL probe that cannot reach its distro returns the same value as one that asked and got "no". Downstream nothing can tell them apart, so a distro that was busy for a second reports no git, or no agent sessions, until relaunch — sticky, silent, and identical to the real thing. That has shipped three times: preflight CLI probes, the glab auth fallback (#8941), and listRunningWslDistrosAsync failing closed with no last-known-good while polled every 2s (PR #17072). Scan the WSL and preflight probe modules for the shape and hold the current set in an allowlist that only shrinks. Scoped deliberately: the same shape appears ~850 times across src/ and is usually correct, because for most callers a failure really does mean absent. It is only dangerous where the answer describes a distro. The guard cannot see the dangerous part — whether the value is later cached or gates discovery is dataflow, not syntax. It stops a new swallow site appearing here without someone saying why it is safe to pin, which is the review that was missing all three times. * test(wsl): make ratchet failures actionable A red build must say what to do. Name the offending files, say the allowlist is where a safe case goes, and — for a stale entry — say the change is fine and the list just needs to shrink. * docs(wsl): track the probe failure-semantics reference docs/** is gitignored with an explicit allowlist, so the reference the ratchet points contributors to was silently left out of the branch. A guard whose error message cites a doc that is not in the repo is worse than no doc. * test(wsl): catch a swallow whose reason trails the return The guard only tolerated comments before `return`, so `return false // ...` slipped past — including the exact snippet the doc and the test's own docstring use as the canonical example. The doc asks authors to write down why a swallow is safe, and the natural place for that sentence is trailing the return, so following the guidance defeated the guard. Verified against both shapes: trailing comment and comment on the line after.
…blyai#17352) * test(wsl): guard probes that report failure as a negative answer A WSL probe that cannot reach its distro returns the same value as one that asked and got "no". Downstream nothing can tell them apart, so a distro that was busy for a second reports no git, or no agent sessions, until relaunch — sticky, silent, and identical to the real thing. That has shipped three times: preflight CLI probes, the glab auth fallback (stablyai#8941), and listRunningWslDistrosAsync failing closed with no last-known-good while polled every 2s (PR stablyai#17072). Scan the WSL and preflight probe modules for the shape and hold the current set in an allowlist that only shrinks. Scoped deliberately: the same shape appears ~850 times across src/ and is usually correct, because for most callers a failure really does mean absent. It is only dangerous where the answer describes a distro. The guard cannot see the dangerous part — whether the value is later cached or gates discovery is dataflow, not syntax. It stops a new swallow site appearing here without someone saying why it is safe to pin, which is the review that was missing all three times. * test(wsl): make ratchet failures actionable A red build must say what to do. Name the offending files, say the allowlist is where a safe case goes, and — for a stale entry — say the change is fine and the list just needs to shrink. * docs(wsl): track the probe failure-semantics reference docs/** is gitignored with an explicit allowlist, so the reference the ratchet points contributors to was silently left out of the branch. A guard whose error message cites a doc that is not in the repo is worse than no doc. * test(wsl): catch a swallow whose reason trails the return The guard only tolerated comments before `return`, so `return false // ...` slipped past — including the exact snippet the doc and the test's own docstring use as the canonical example. The doc asks authors to write down why a swallow is safe, and the natural place for that sentence is trailing the return, so following the guidance defeated the guard. Verified against both shapes: trailing comment and comment on the line after.
* fix(wsl): scan sessions only in running distros * test(ai-vault): pin WSL discovery platform * fix(wsl): suspend transcript watchers for stopped distros * test(wsl): pin transcript scan gate platform * fix(wsl): settle stopped transcript loading * fix(wsl): add last-known-good fallback and backoff to running-distro discovery listRunningWslDistrosAsync failed closed on any probe error (timeout, ENOENT, wsl.exe hiccup), indistinguishable from "no distros running". A 2s poll (wsl-transcript-running-observer.ts) calls it indefinitely while any WSL transcript tab is open, so a persistently broken wsl.exe silently made every WSL session vanish app-wide with no way to tell "discovery broken" from "distro stopped", and re-spawned wsl.exe every 2s forever. Extract a dedicated cache/backoff module (wsl-running-distro-cache.ts, mirroring the sibling machinery already in wsl.ts for the full distro list) so a probe failure falls back to the last-known-good running-distro list and backs off further probes, while a genuine empty result (no distros running) stays authoritative. Add a consumer-level test simulating a sustained wsl.exe outage across a live transcript-watcher polling session, asserting the observer keeps reporting "running" and that real wsl.exe spawns stay bounded. * fix(build): list the new WSL cache module in the web typecheck project config/tsconfig.tc.web.json enumerates its files explicitly, so a new module imported by wsl.ts fails the full typecheck with TS6307 until it is listed. pnpm tc:node passes without it, which is how this got missed. src/main/wsl.ts(13,8): error TS6307: File 'src/main/wsl-running-distro-cache.ts' is not listed within the file list of project 'config/tsconfig.tc.web.json'. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
ELI5
Agent Session History currently asks every installed WSL distro for its home directory. Asking a stopped distro boots it. This change first asks WSL which distros are already running and only scans those, so a history refresh cannot start a sleeping VM.
What Changed
wsl.exe --list --running --quietinstead of enumerating every installed distro.No setting, renderer UI, IPC/RPC shape, or remote-wire behavior changes.
Why
getAiVaultWslHomeDirs()on current main callswsl.exe -d <distro> ... echo $HOMEfor every installed distro.wsl.exe -dnecessarily starts a stopped distro, so opening or refreshing Agent Session History can leavevmmemWSLresident even when the user was not using WSL.The ownership boundary is WSL discovery, before any consumer can issue a distro-starting command. A default-on opt-out such as #16961 leaves users affected until they discover a setting and requires settings/UI/cache plumbing around the underlying behavior. Running-only discovery removes the side effect for every consumer while still showing sessions whenever the user has started that distro themselves.
This complements #15475, which gates scanner filesystem walks, and #16050, which adds WSL Codex usage lanes. This PR covers the earlier home/root discovery and native-chat/watch paths that can start a distro before a scanner walk.
The broader Windows freeze report in #16038 was investigated separately. A bounded current-main stress run did not establish WSL scanning as its cause, so this PR intentionally makes no broad-freeze claim or unrelated performance change.
Behavior Changes Not Covered Above
Two additional behavior changes ship with this PR that were not called out above:
fs.watchis disabled entirely for WSL transcript paths —wsl-transcript-watcher-running-guard.tsreturns a no-op watcher (bindalwaysfalse) for any WSL UNC path.transcript-watch-engine.tsalso skips the normal 1s reconciliation timer for WSL paths (if (!isWslPath) { scheduler.startReconciliation() }) and instead drives every re-read off the 2s running-distro poll inwsl-transcript-running-observer.ts. Net effect: live-tail latency for WSL-hosted transcripts goes from ~1s event-driven (native watch) to a strict 2s poll interval.session-file-resolver.ts, when the hook-providedtranscript_pathneeds WSL host resolution (needsWslHostResolution(hookPath)) and that resolution attempt fails or comes back empty,resolveSessionFilePathreturnsnullor throws immediately — it does not fall through toresolveSessionFileById's id-based search. This applies even when the failure was a single transient timeout against a distro that is otherwise running, not just when the distro is genuinely stopped.Neither change is gated by a setting.
Linked Issue
Fixes #16959
Related: #16038, #16961
Visual Proof
N/A — backend WSL discovery behavior only; there is no UI change.
Physical Windows before/after/revert oracle using an isolated distro:
The cached-UNC watcher oracle also stayed stopped with the candidate and started the distro when its gate was reverted.
Testing
Windows validation:
pnpm tc: passed.git diff --check: passed.Stopped.The
check:code-quality:changedwrapper is not usable in this Node 24/Windows environment because itsspawn pnpm.cmd/npx.cmdcall fails withEINVAL; the underlying typecheck, changed-file oxlint, tests, and diff check were run directly.Platforms exercised: physical Windows 11 + WSL2 and Electron on Windows. Non-Windows returns the existing no-WSL result. SSH, folder workspaces, and mixed-version clients do not cross this local discovery path or any changed wire contract.
Review
Agent skill upstream boundary
Notes
The isolated test distro was unregistered after validation. Unrelated host distros were not started or modified.
Checklist
N/Awith reason