Harden state management against stale and missing entries
The registry lock could spin silently forever on contention, worktrees could vanish between runs, and loadAll swallowed errors from projects whose state files were removed. Also skip symlinks during scan to avoid cycles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3f8a3839f1
commit
7bea6f376b
|
|
@ -1,5 +1,6 @@
|
|||
import { basename } from "path"
|
||||
import { homedir } from "os"
|
||||
import { exists } from "fs/promises"
|
||||
import * as git from "../git.ts"
|
||||
import * as vm from "../vm.ts"
|
||||
import * as state from "../state.ts"
|
||||
|
|
@ -46,6 +47,7 @@ async function resolveStatus(
|
|||
s: { branch: string; worktree: string; in_review?: boolean },
|
||||
vmRunning: boolean,
|
||||
): Promise<string> {
|
||||
if (!(await exists(s.worktree))) return "idle"
|
||||
if (vmRunning) {
|
||||
const active = await vm.isClaudeActive(s.worktree, s.branch)
|
||||
if (active && s.in_review) return "review"
|
||||
|
|
|
|||
30
src/state.ts
30
src/state.ts
|
|
@ -66,12 +66,14 @@ const GLOBAL_STATE_PATH = join(GLOBAL_DIR, "registry.json")
|
|||
async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const lockPath = join(GLOBAL_DIR, "registry.lock")
|
||||
await mkdir(GLOBAL_DIR, { recursive: true })
|
||||
let acquired = false
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
await mkdir(lockPath)
|
||||
acquired = true
|
||||
break
|
||||
} catch {
|
||||
// If the lock is older than 30 seconds, assume it's stale (crashed process)
|
||||
// If the lock is older than 5 minutes, assume it's stale (crashed process)
|
||||
try {
|
||||
const info = await stat(lockPath)
|
||||
if (Date.now() - info.mtimeMs > 300_000) {
|
||||
|
|
@ -83,6 +85,7 @@ async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
|||
await Bun.sleep(50)
|
||||
}
|
||||
}
|
||||
if (!acquired) throw new Error("Could not acquire registry lock")
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
|
|
@ -161,7 +164,7 @@ export async function scanAndRegister(dir: string, maxDepth = 5): Promise<string
|
|||
}
|
||||
}
|
||||
|
||||
const children = entries.filter(e => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
|
||||
const children = entries.filter(e => e.isDirectory() && !e.isSymbolicLink() && !e.name.startsWith(".") && e.name !== "node_modules")
|
||||
await Promise.all(children.map(entry => walk(join(d, entry.name), depth + 1)))
|
||||
}
|
||||
|
||||
|
|
@ -190,19 +193,36 @@ export interface GlobalSession extends Session {
|
|||
repoRoot: string
|
||||
}
|
||||
|
||||
/** Load all sessions across all registered projects. */
|
||||
/** Load all sessions across all registered projects. Prunes stale entries. */
|
||||
export async function loadAll(): Promise<GlobalSession[]> {
|
||||
const gs = await loadGlobal()
|
||||
const all: GlobalSession[] = []
|
||||
const stale: string[] = []
|
||||
|
||||
for (const project of gs.projects) {
|
||||
try {
|
||||
const st = await load(project)
|
||||
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 {}
|
||||
} catch {
|
||||
stale.push(project)
|
||||
}
|
||||
}
|
||||
|
||||
// Prune projects whose state files no longer exist
|
||||
if (stale.length > 0) {
|
||||
withGlobalLock(async () => {
|
||||
const fresh = await loadGlobal()
|
||||
fresh.projects = fresh.projects.filter(p => !stale.includes(p))
|
||||
await saveGlobal(fresh)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
return all
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user