mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(worktrees): trace a WSL repo's Windows-side workspaces into the distro too
Distro discovery keyed only on the workspace root being a UNC path, but a WSL repo can legitimately own workspaces under C:\. computeWorkspaceRootAsync mirrors the workspace dir into the distro only when the distro home resolves at create time; when that wsl.exe call fails it falls back to the drive path, and those workspaces stay on the Windows side. The agent is still spawned through wsl.exe, so its cwd is the drvfs mirror (/mnt/c/...) and its bucket lands in the distro's own ~/.claude/projects, where the host-home scan cannot see it. The scan then reported complete and memoized the empty answer, so the name was reissued and the next occupant inherited the previous conversation — the STA-4472 defect, in the one configuration the UNC check does not cover. Which distro to look in comes from the repo path rather than the workspace root, since that is what still identifies the distro once the root is a drive path.
This commit is contained in:
@@ -267,7 +267,12 @@ export async function ensureRetiredWorktreeNamesBackfilled(
|
||||
)
|
||||
const scanKey = `${getRepoExecutionHostId(repo)}:${worktreePathComparisonKey(probePath)}`
|
||||
const names = await runRetirementBackfillScan(store, scanKey, () =>
|
||||
discoverRetiredWorktreeNames({ workspaceRoots: [parentPath(probePath)] })
|
||||
discoverRetiredWorktreeNames({
|
||||
workspaceRoots: [parentPath(probePath)],
|
||||
// The repo, not the workspace root, is what says which distro the agent runs in: a WSL repo
|
||||
// whose workspaces fell back to the Windows side still buckets under the drvfs mirror there.
|
||||
wslDistro: parseWslPath(repo.path)?.distro
|
||||
})
|
||||
)
|
||||
// Why the merge sits outside the cached scan: the scan is per cwd namespace but the registry is
|
||||
// per repo, so every repo that asks must receive it — not only the one that triggered it.
|
||||
|
||||
@@ -109,6 +109,33 @@ describe('retirement discovery on WSL', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('traces a Windows-side workspace of a WSL repo into the distro through the drvfs mirror', async () => {
|
||||
// `computeWorkspaceRootAsync` falls back to the drive path whenever the distro home will not
|
||||
// resolve at create time, so a WSL repo can own workspaces under `C:\`. The agent is still
|
||||
// spawned through `wsl.exe`, so its cwd is `/mnt/c/...` and its bucket lands in the distro.
|
||||
const distroHome = await mkdtemp(join(tmpdir(), 'orca-drvfs-distro-'))
|
||||
const hostHome = await mkdtemp(join(tmpdir(), 'orca-drvfs-host-'))
|
||||
try {
|
||||
await mkdir(
|
||||
join(distroHome, '.claude', 'projects', `-mnt-c-users-ada-orca-workspaces-${FIRST}`),
|
||||
{ recursive: true }
|
||||
)
|
||||
|
||||
const retired = await discoverRetiredWorktreeNames({
|
||||
workspaceRoots: ['C:\\Users\\ada\\orca\\workspaces'],
|
||||
home: hostHome,
|
||||
env: {},
|
||||
wslDistro: 'Ubuntu',
|
||||
resolveWslHome: async (distro) => (distro === 'Ubuntu' ? distroHome : null)
|
||||
})
|
||||
|
||||
expect(retired.names).toEqual(new Set([FIRST]))
|
||||
} finally {
|
||||
await rm(distroHome, { force: true, recursive: true })
|
||||
await rm(hostHome, { force: true, recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
@@ -93,6 +93,12 @@ type ClaudeProjectsSource = {
|
||||
encodedParents: string[]
|
||||
}
|
||||
|
||||
/** How a distro sees a Windows drive path through the drvfs automount, which is lowercase `/mnt`. */
|
||||
function drvfsPath(windowsPath: string): string | null {
|
||||
const drive = windowsPath.match(/^([A-Za-z]):[/\\](.*)$/)
|
||||
return drive ? `/mnt/${drive[1].toLowerCase()}/${drive[2].replace(/\\/g, '/')}` : null
|
||||
}
|
||||
|
||||
// Lowercased on both sides: the recorded cwd can differ in case from ours on case-insensitive
|
||||
// filesystems, and a missed bucket reissues a live path while a spurious one costs one name.
|
||||
function encodeParents(paths: readonly string[]): string[] {
|
||||
@@ -116,6 +122,7 @@ async function claudeProjectsSources(args: {
|
||||
home?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
resolveWslHome?: (distro: string) => Promise<string | null>
|
||||
wslDistro?: string
|
||||
}): Promise<{ sources: ClaudeProjectsSource[]; complete: boolean }> {
|
||||
const sources: ClaudeProjectsSource[] = []
|
||||
let complete = true
|
||||
@@ -127,13 +134,22 @@ async function claudeProjectsSources(args: {
|
||||
})
|
||||
}
|
||||
const linuxRootsByDistro = new Map<string, string[]>()
|
||||
const addLinuxRoot = (distro: string, linuxPath: string): void => {
|
||||
linuxRootsByDistro.set(distro, [...(linuxRootsByDistro.get(distro) ?? []), linuxPath])
|
||||
}
|
||||
for (const root of args.workspaceRoots) {
|
||||
const wsl = parseWslUncPath(root)
|
||||
if (wsl) {
|
||||
linuxRootsByDistro.set(wsl.distro, [
|
||||
...(linuxRootsByDistro.get(wsl.distro) ?? []),
|
||||
wsl.linuxPath
|
||||
])
|
||||
addLinuxRoot(wsl.distro, wsl.linuxPath)
|
||||
continue
|
||||
}
|
||||
// A WSL repo can still have its workspaces on the Windows side: `computeWorkspaceRootAsync`
|
||||
// falls back to the drive path whenever the distro home will not resolve at create time. The
|
||||
// agent is still spawned through `wsl.exe`, so its cwd is the drvfs mirror of that drive path
|
||||
// and its bucket lands in the distro home under `-mnt-c-…`, out of reach of the host scan.
|
||||
const mirrored = args.wslDistro ? drvfsPath(root) : null
|
||||
if (mirrored && args.wslDistro) {
|
||||
addLinuxRoot(args.wslDistro, mirrored)
|
||||
}
|
||||
}
|
||||
const resolveWslHome = args.resolveWslHome ?? getWslHomeAsync
|
||||
@@ -163,6 +179,8 @@ export async function discoverRetiredWorktreeNames(args: {
|
||||
env?: NodeJS.ProcessEnv
|
||||
/** Injected in tests; production resolves the distro's `$HOME` as a UNC path. */
|
||||
resolveWslHome?: (distro: string) => Promise<string | null>
|
||||
/** The repo's distro, so workspaces that live on the Windows side are still traced into it. */
|
||||
wslDistro?: string
|
||||
}): Promise<RetirementScanResult> {
|
||||
const leafNames: string[] = []
|
||||
let refused = false
|
||||
|
||||
Reference in New Issue
Block a user