diff --git a/src/commands/list.ts b/src/commands/list.ts index 1970c84..a89b466 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -59,18 +59,6 @@ async function resolveStatus( return commits ? "saved" : "idle" } -async function clearStaleReviews(staleReviews: { repoRoot: string; branch: string }[]) { - if (staleReviews.length === 0) return - const byRepo = Map.groupBy(staleReviews, r => r.repoRoot) - for (const [repoRoot, reviews] of byRepo) { - const fresh = await state.load(repoRoot) - for (const { branch } of reviews) { - if (fresh.sessions[branch]) fresh.sessions[branch].in_review = false - } - await state.save(repoRoot, fresh).catch(() => {}) - } -} - async function resolveAllStatuses( sessions: T[], vmRunning: boolean, @@ -80,14 +68,24 @@ async function resolveAllStatuses [keyFn(s), results[i]])) // Clear stale reviews: in_review is set but Claude isn't actually active - const staleReviews: { repoRoot: string; branch: string }[] = [] + // Re-load state before saving to narrow the race window with concurrent writers + const staleByRepo = new Map() for (let i = 0; i < sessions.length; i++) { if (sessions[i].in_review && results[i] !== "review") { sessions[i].in_review = false - staleReviews.push({ repoRoot: sessions[i].repoRoot, branch: sessions[i].branch }) + const arr = staleByRepo.get(sessions[i].repoRoot) ?? [] + arr.push(sessions[i].branch) + staleByRepo.set(sessions[i].repoRoot, arr) } } - await clearStaleReviews(staleReviews) + for (const [repoRoot, branches] of staleByRepo) { + const fresh = await state.load(repoRoot) + for (const branch of branches) { + if (fresh.sessions[branch]) fresh.sessions[branch].in_review = false + } + await state.save(repoRoot, fresh).catch(() => {}) + } + return statuses } diff --git a/src/state.ts b/src/state.ts index 63f491a..59db416 100644 --- a/src/state.ts +++ b/src/state.ts @@ -85,7 +85,6 @@ async function withGlobalLock(fn: () => Promise): Promise { await Bun.sleep(50) } } - if (!acquired) throw new Error("Could not acquire registry lock") try { return await fn() } finally { @@ -107,7 +106,6 @@ async function loadGlobal(): Promise { } async function saveGlobal(gs: GlobalState): Promise { - await mkdir(GLOBAL_DIR, { recursive: true }) const tmpPath = GLOBAL_STATE_PATH + ".tmp" await Bun.write(tmpPath, JSON.stringify(gs, null, 2) + "\n") await rename(tmpPath, GLOBAL_STATE_PATH) @@ -175,9 +173,8 @@ export async function scanAndRegister(dir: string, maxDepth = 5): Promise { const stale: string[] = [] for (const project of gs.projects) { - try { + const st = await load(project) + if (Object.keys(st.sessions).length === 0) { + // Check if the state file actually exists — if not, this project is stale const file = Bun.file(join(project, ".sandlot", "state.json")) if (!(await file.exists())) { stale.push(project) continue } - const st: State = await file.json() - const repo = basename(project) - for (const session of Object.values(st.sessions)) { - all.push({ ...session, repo, repoRoot: project }) - } - } catch { - stale.push(project) + } + const repo = basename(project) + for (const session of Object.values(st.sessions)) { + all.push({ ...session, repo, repoRoot: project }) } } // Prune projects whose state files no longer exist if (stale.length > 0) { - withGlobalLock(async () => { + await withGlobalLock(async () => { const fresh = await loadGlobal() fresh.projects = fresh.projects.filter(p => !stale.includes(p)) await saveGlobal(fresh)