diff --git a/src/commands/list.ts b/src/commands/list.ts index e9c986e..14d615c 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -39,13 +39,15 @@ export async function action(opts: { json?: boolean }) { } // Determine status for each session in parallel - const staleReviewSessions: state.Session[] = [] const statusEntries = await Promise.all( sessions.map(async (s): Promise<[string, string]> => { const active = await vm.isClaudeActive(s.worktree, s.branch) if (active && s.in_review) return [s.branch, "review"] - // Collect stale in_review flags for batch self-heal below - if (!active && s.in_review) staleReviewSessions.push(s) + if (!active && s.in_review) { + // Self-heal stale in_review flag (fire-and-forget) + state.patchSession(root, s.branch, { in_review: false }).catch(() => {}) + s.in_review = false + } if (active) return [s.branch, "active"] const dirty = await git.isDirty(s.worktree) if (dirty) return [s.branch, "dirty"] @@ -55,12 +57,6 @@ export async function action(opts: { json?: boolean }) { ) const statuses = Object.fromEntries(statusEntries) - // Self-heal stale in_review flags in parallel and update in-memory objects - await Promise.all(staleReviewSessions.map(async (s) => { - await state.patchSession(root, s.branch, { in_review: false }).catch(() => {}) - s.in_review = false - })) - if (opts.json) { const withStatus = sessions.map(s => ({ ...s, status: statuses[s.branch] })) console.log(JSON.stringify(withStatus, null, 2)) diff --git a/src/commands/review.ts b/src/commands/review.ts index 66321e7..5ef95b4 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -80,13 +80,9 @@ Your thoughts, in brief. spin.succeed("Session ready") await vm.claude(session.worktree, { prompt }) } - } catch (e) { - spin.stop() - throw e } finally { - // Clear review flag before saveChanges to minimize the race window + spin.stop() await state.patchSession(root, session.branch, { in_review: false }).catch(() => {}) - // Save worktree changes only in interactive mode - if (!opts.print) await saveChanges(session.worktree, session.branch) + if (!opts.print) await saveChanges(session.worktree, session.branch).catch(() => {}) } } diff --git a/src/state.ts b/src/state.ts index a9b2764..4708d26 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,5 +1,5 @@ import { join, dirname } from "path" -import { readdir, rename, mkdir, rmdir } from "fs/promises" +import { readdir, rename } from "fs/promises" import { homedir } from "os" export interface Session { @@ -36,64 +36,28 @@ export async function save(repoRoot: string, state: State): Promise { await rename(tmpPath, path) } -const LOCK_TIMEOUT = 5000 -const LOCK_RETRY_MS = 50 - -async function withStateLock(repoRoot: string, fn: () => Promise): Promise { - const lockPath = statePath(repoRoot) + ".lock" - const start = Date.now() - - while (true) { - try { - await mkdir(lockPath) - break - } catch (e: any) { - if (e.code !== "EEXIST") throw e - if (Date.now() - start > LOCK_TIMEOUT) { - // Stale lock — force acquire - await rmdir(lockPath).catch(() => {}) - await mkdir(lockPath) - break - } - await Bun.sleep(LOCK_RETRY_MS) - } - } - - try { - return await fn() - } finally { - await rmdir(lockPath).catch(() => {}) - } -} - export async function getSession(repoRoot: string, branch: string): Promise { - const state = await load(repoRoot) - return state.sessions[branch] + const st = await load(repoRoot) + return st.sessions[branch] } export async function setSession(repoRoot: string, session: Session): Promise { - await withStateLock(repoRoot, async () => { - const state = await load(repoRoot) - state.sessions[session.branch] = session - await save(repoRoot, state) - }) + const st = await load(repoRoot) + st.sessions[session.branch] = session + await save(repoRoot, st) } export async function patchSession(repoRoot: string, branch: string, patch: Partial): Promise { - await withStateLock(repoRoot, async () => { - const state = await load(repoRoot) - if (!state.sessions[branch]) throw new Error(`session not found: ${branch}`) - Object.assign(state.sessions[branch], patch) - await save(repoRoot, state) - }) + const st = await load(repoRoot) + if (!st.sessions[branch]) throw new Error(`session not found: ${branch}`) + Object.assign(st.sessions[branch], patch) + await save(repoRoot, st) } export async function removeSession(repoRoot: string, branch: string): Promise { - await withStateLock(repoRoot, async () => { - const state = await load(repoRoot) - delete state.sessions[branch] - await save(repoRoot, state) - }) + const st = await load(repoRoot) + delete st.sessions[branch] + await save(repoRoot, st) } export interface GlobalSession extends Session {