fix(worktrees): prune stale worktree mappings

Fall back to root when a mapped worktree slug is missing and persistently remove missing slugs from the worktree map to prevent proxy 404s.
This commit is contained in:
Shantur Rathore
2026-02-07 15:55:35 +00:00
parent 3cfaf689e7
commit bdd3fe8899

View File

@@ -45,6 +45,11 @@ async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null) next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null)
return next return next
}) })
// If we already loaded a worktree mapping, drop stale slugs.
if (worktreeMapByInstance().has(instanceId)) {
void pruneWorktreeMap(instanceId).catch(() => undefined)
}
}) })
.catch((error) => { .catch((error) => {
log.warn("Failed to load worktrees", { instanceId, error }) log.warn("Failed to load worktrees", { instanceId, error })
@@ -86,6 +91,10 @@ async function reloadWorktrees(instanceId: string): Promise<void> {
next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null) next.set(instanceId, typeof response.isGitRepo === "boolean" ? response.isGitRepo : null)
return next return next
}) })
if (worktreeMapByInstance().has(instanceId)) {
void pruneWorktreeMap(instanceId).catch(() => undefined)
}
}) })
.catch((error) => { .catch((error) => {
log.warn("Failed to reload worktrees", { instanceId, error }) log.warn("Failed to reload worktrees", { instanceId, error })
@@ -132,6 +141,11 @@ async function ensureWorktreeMapLoaded(instanceId: string): Promise<void> {
next.set(instanceId, normalizeMap(map)) next.set(instanceId, normalizeMap(map))
return next return next
}) })
// If worktrees are already loaded, prune any mappings that reference missing worktrees.
if (worktreesByInstance().has(instanceId)) {
void pruneWorktreeMap(instanceId).catch(() => undefined)
}
}) })
.catch((error) => { .catch((error) => {
log.warn("Failed to load worktree map", { instanceId, error }) log.warn("Failed to load worktree map", { instanceId, error })
@@ -173,14 +187,50 @@ function getWorktreeMap(instanceId: string): WorktreeMap {
return worktreeMapByInstance().get(instanceId) ?? normalizeMap(null) return worktreeMapByInstance().get(instanceId) ?? normalizeMap(null)
} }
function getDefaultWorktreeSlug(instanceId: string): string { function isWorktreeSlugAvailable(instanceId: string, slug: string): boolean {
return getWorktreeMap(instanceId).defaultWorktreeSlug || "root" const normalized = (slug ?? "").trim() || "root"
if (normalized === "root") return true
const list = getWorktrees(instanceId)
// If worktrees aren't loaded yet, don't force root incorrectly.
if (list.length === 0) return true
return list.some((wt) => wt.slug === normalized)
} }
async function setDefaultWorktreeSlug(instanceId: string, slug: string): Promise<void> { function normalizeWorktreeSlug(instanceId: string, slug: string): string {
await ensureWorktreeMapLoaded(instanceId) const normalized = (slug ?? "").trim() || "root"
if (normalized === "root") return "root"
return isWorktreeSlugAvailable(instanceId, normalized) ? normalized : "root"
}
async function pruneWorktreeMap(instanceId: string): Promise<boolean> {
const current = getWorktreeMap(instanceId) const current = getWorktreeMap(instanceId)
const next: WorktreeMap = { ...current, defaultWorktreeSlug: slug } const available = new Set(getWorktrees(instanceId).map((wt) => wt.slug))
available.add("root")
let changed = false
let nextDefault = current.defaultWorktreeSlug || "root"
if (!available.has(nextDefault)) {
nextDefault = "root"
changed = true
}
const nextMapping: Record<string, string> = { ...(current.parentSessionWorktreeSlug ?? {}) }
for (const [sessionId, slug] of Object.entries(nextMapping)) {
if (!available.has(slug)) {
delete nextMapping[sessionId]
changed = true
}
}
if (!changed) return false
const next: WorktreeMap = {
version: 1,
defaultWorktreeSlug: nextDefault,
parentSessionWorktreeSlug: nextMapping,
}
setWorktreeMapByInstance((prev) => { setWorktreeMapByInstance((prev) => {
const map = new Map(prev) const map = new Map(prev)
map.set(instanceId, next) map.set(instanceId, next)
@@ -188,7 +238,29 @@ async function setDefaultWorktreeSlug(instanceId: string, slug: string): Promise
}) })
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => { await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist default worktree", { instanceId, slug, error }) log.warn("Failed to persist pruned worktree map", { instanceId, error })
})
return true
}
function getDefaultWorktreeSlug(instanceId: string): string {
return normalizeWorktreeSlug(instanceId, getWorktreeMap(instanceId).defaultWorktreeSlug || "root")
}
async function setDefaultWorktreeSlug(instanceId: string, slug: string): Promise<void> {
await ensureWorktreeMapLoaded(instanceId)
const current = getWorktreeMap(instanceId)
const nextSlug = normalizeWorktreeSlug(instanceId, slug)
const next: WorktreeMap = { ...current, defaultWorktreeSlug: nextSlug }
setWorktreeMapByInstance((prev) => {
const map = new Map(prev)
map.set(instanceId, next)
return map
})
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist default worktree", { instanceId, slug: nextSlug, error })
}) })
} }
@@ -200,7 +272,8 @@ function getParentSessionId(instanceId: string, sessionId: string): string {
function getWorktreeSlugForParentSession(instanceId: string, parentSessionId: string): string { function getWorktreeSlugForParentSession(instanceId: string, parentSessionId: string): string {
const map = getWorktreeMap(instanceId) const map = getWorktreeMap(instanceId)
return map.parentSessionWorktreeSlug[parentSessionId] ?? map.defaultWorktreeSlug ?? "root" const candidate = map.parentSessionWorktreeSlug[parentSessionId] ?? map.defaultWorktreeSlug ?? "root"
return normalizeWorktreeSlug(instanceId, candidate)
} }
function getWorktreeSlugForSession(instanceId: string, sessionId: string): string { function getWorktreeSlugForSession(instanceId: string, sessionId: string): string {
@@ -211,8 +284,9 @@ function getWorktreeSlugForSession(instanceId: string, sessionId: string): strin
async function setWorktreeSlugForParentSession(instanceId: string, parentSessionId: string, slug: string): Promise<void> { async function setWorktreeSlugForParentSession(instanceId: string, parentSessionId: string, slug: string): Promise<void> {
await ensureWorktreeMapLoaded(instanceId) await ensureWorktreeMapLoaded(instanceId)
const current = getWorktreeMap(instanceId) const current = getWorktreeMap(instanceId)
const normalizedSlug = normalizeWorktreeSlug(instanceId, slug)
const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) } const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) }
nextMapping[parentSessionId] = slug nextMapping[parentSessionId] = normalizedSlug
const next: WorktreeMap = { ...current, parentSessionWorktreeSlug: nextMapping } const next: WorktreeMap = { ...current, parentSessionWorktreeSlug: nextMapping }
setWorktreeMapByInstance((prev) => { setWorktreeMapByInstance((prev) => {
const map = new Map(prev) const map = new Map(prev)
@@ -221,7 +295,7 @@ async function setWorktreeSlugForParentSession(instanceId: string, parentSession
}) })
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => { await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist session worktree mapping", { instanceId, parentSessionId, slug, error }) log.warn("Failed to persist session worktree mapping", { instanceId, parentSessionId, slug: normalizedSlug, error })
}) })
} }
@@ -251,13 +325,14 @@ function getWorktreeSlugForDirectory(instanceId: string, directory: string | und
} }
function buildWorktreeProxyPath(instanceId: string, slug: string): string { function buildWorktreeProxyPath(instanceId: string, slug: string): string {
const normalizedSlug = slug || "root" const normalizedSlug = normalizeWorktreeSlug(instanceId, slug || "root")
return `/workspaces/${encodeURIComponent(instanceId)}/worktrees/${encodeURIComponent(normalizedSlug)}/instance` return `/workspaces/${encodeURIComponent(instanceId)}/worktrees/${encodeURIComponent(normalizedSlug)}/instance`
} }
function getOrCreateWorktreeClient(instanceId: string, slug: string): OpencodeClient { function getOrCreateWorktreeClient(instanceId: string, slug: string): OpencodeClient {
const proxyPath = buildWorktreeProxyPath(instanceId, slug) const normalized = normalizeWorktreeSlug(instanceId, slug || "root")
return sdkManager.createClient(instanceId, proxyPath, slug) const proxyPath = buildWorktreeProxyPath(instanceId, normalized)
return sdkManager.createClient(instanceId, proxyPath, normalized)
} }
function getRootClient(instanceId: string): OpencodeClient { function getRootClient(instanceId: string): OpencodeClient {