From 6bdb604dc466830f7ff57b0eb6f02087509096e5 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:41:15 -0700 Subject: [PATCH] perf(worktrees): probe reachability with stat, and record that the gate coupling runs both ways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/main/worktree-retirement-discovery.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/worktree-retirement-discovery.ts b/src/main/worktree-retirement-discovery.ts index a8a0cb69b81..3392e4cae93 100644 --- a/src/main/worktree-retirement-discovery.ts +++ b/src/main/worktree-retirement-discovery.ts @@ -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 { 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