fix(worktrees): stop trusting a UNC ENOENT, which is what a shut-down distro looks like

Windows reports an unreachable 9P route as ENOENT, so a distro that has merely been shut down is
indistinguishable from one that never held any buckets. wsl.ts already refuses to trust a UNC
ENOENT for the same reason, probing inside the distro instead.

The classification added earlier called ENOENT a complete answer, which is right for a local home
that simply has no agent state but wrong here. After a wsl --shutdown the cached distro home still
resolves, so nothing else marked the scan incomplete: the empty result was memoized for the whole
process lifetime and every later generated create in that namespace reissued names spent inside
the distro. That is STA-4472 again, reached by a different route.

ENOENT now only means "absent" off UNC.
This commit is contained in:
Brennan Benson
2026-08-20 00:40:24 -07:00
parent aca7599219
commit 7fb76bd7ef
2 changed files with 25 additions and 2 deletions
@@ -109,6 +109,23 @@ describe('retirement discovery on WSL', () => {
}
})
it('does not trust a UNC ENOENT, which is what a shut-down distro looks like', async () => {
// Windows maps an unreachable 9P route to ENOENT, so `wsl --shutdown` is indistinguishable
// from a distro that never held buckets. The home stays cached and still resolves, so nothing
// else marks the scan incomplete — believing ENOENT here memoizes the STA-4472 hole for good.
const enoent = Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
runWslTranscriptFsTaskMock.mockRejectedValue(enoent)
const retired = await discoverRetiredWorktreeNames({
workspaceRoots: [DISTRO_ROOT],
home: '/nonexistent-home',
env: {},
resolveWslHome: async () => '\\\\wsl.localhost\\Ubuntu\\home\\ada'
})
expect(retired.complete).toBe(false)
})
it('stays retryable when the distro home cannot be resolved, rather than memoizing the hole', async () => {
// `getWslHomeAsync` shells out to `wsl.exe` and returns null for a stopped or slow distro.
// Memoizing that as a complete "nothing is retired" is the STA-4472 defect itself, since the
+8 -2
View File
@@ -1,6 +1,6 @@
import { homedir } from 'node:os'
import { join } from 'node:path'
import { parseWslUncPath } from '../shared/wsl-paths'
import { isWslUncPath, parseWslUncPath } from '../shared/wsl-paths'
import {
encodeClaudeProjectPaths,
isClaudeProjectDirInScope
@@ -68,8 +68,14 @@ async function readDirectoryNames(path: string): Promise<{ names: string[]; refu
// A missing root is itself a complete answer: the agent may never have run on this machine.
// Anything else — a gate refusal, an EIO on a redirected or network home — leaves names
// unread, and memoizing that as "nothing is retired" is the under-retiring direction.
//
// Never on UNC though: Windows reports an unreachable 9P route as ENOENT, so a distro that is
// merely shut down is indistinguishable from one that never held buckets. `wsl.ts` already
// refuses to trust a UNC ENOENT for the same reason. Believing it here would memoize the
// STA-4472 hole for the process lifetime, since the cached distro home still resolves.
const code = (error as NodeJS.ErrnoException | null)?.code
return { names: [], refused: code !== 'ENOENT' && code !== 'ENOTDIR' }
const absent = code === 'ENOENT' || code === 'ENOTDIR'
return { names: [], refused: !absent || isWslUncPath(path) }
}
}