perf(worktrees): probe reachability with stat, and record that the gate coupling runs both ways

The ancestor probe only asks whether a directory is there, but it listed it — enumerating a WSL
home over 9P, on the composer-open path, holding the single scan permit while it did. stat answers
the same question; the gate already supports the operation.

Also corrects the trade-off note added last commit, which recorded only the direction where
retirement discovery is the victim. Because the gate stuck-check matches scan against scan
regardless of route, the reverse is now true too and is the part this PR introduces: a retirement
listing wedged on one distro can fast-fail transcript discovery on a healthy one.
This commit is contained in:
Brennan Benson
2026-08-20 00:40:25 -07:00
parent 2ab5910394
commit 6bdb604dc4
+12 -9
View File
@@ -5,7 +5,7 @@ import {
encodeClaudeProjectPaths,
isClaudeProjectDirInScope
} from './ai-vault/claude-project-dir-encoding'
import { wslGatedReaddir } from './native-chat/wsl-transcript-fs-access'
import { wslGatedReaddir, wslGatedStat } from './native-chat/wsl-transcript-fs-access'
import type { RetirementScanResult } from './worktree-retirement-backfill-scan'
import { normalizeRetirableGeneratedName } from './worktree-name-retirement'
import { getWslHomeAsync } from './wsl'
@@ -52,11 +52,12 @@ export function extractBucketLeafCandidates(
/** WSL UNC listings go through the shared 9P gate, which bounds concurrency and deadlines each
* call; off UNC it is a plain `readdir`.
*
* Accepted cost of gating: the gate admits one `scan` task process-wide, and its stuck-task
* check matches scan-against-scan regardless of route. So retirement discovery now queues with —
* and, on a wedged distro, can fast-fail — native-chat transcript discovery, which an ungated
* `readdir` never could. It is still the right trade: the gate has the only deadline and permit
* accounting these UNC reads get, and without it a hung 9P route holds a libuv thread outright.
* Accepted cost of gating: the gate admits one `scan` task process-wide and its stuck-task check
* matches scan-against-scan regardless of route, so the coupling runs both ways. Retirement
* discovery can be fast-failed by a wedged transcript scan, and — new here — a retirement listing
* wedged on one distro can fast-fail transcript discovery on a healthy one, which an ungated
* `readdir` never could. It is still the right trade: the gate holds the only deadline and permit
* accounting these UNC reads get, and without it a hung 9P route keeps a libuv thread outright.
* A dedicated lane would need a third priority, which is a change to the gate, not to this file.
*
* A gate refusal is reported rather than thrown. Throwing abandoned the whole scan at whichever
@@ -94,15 +95,17 @@ async function readDirectoryNames(path: string): Promise<{ names: string[]; refu
* cases unreachable, which is the 60s rescan loop this exists to avoid. */
async function uncRouteIsReachable(path: string): Promise<boolean> {
let current = path
// Bounded so a pathological path cannot spend the gate's scan slot indefinitely; the distro root
// is only a few levels above anything discovery lists.
// `stat`, not a listing: this only asks whether an ancestor is there, and enumerating a WSL home
// over 9P to answer that would spend the single scan permit on the composer-open path.
// Bounded so a pathological path cannot hold that permit; the distro root is only a few levels
// above anything discovery lists.
for (let depth = 0; depth < 8; depth += 1) {
const parent = current.replace(/[/\\]+[^/\\]+[/\\]*$/, '')
if (!parent || parent === current || !isWslUncPath(parent)) {
return false
}
try {
await wslGatedReaddir(parent, 'scan')
await wslGatedStat(parent, 'scan')
return true
} catch (error) {
const code = (error as NodeJS.ErrnoException | null)?.code