From a682539db3b3c884860d188fab7e7eb3a98bce80 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 18 Mar 2026 23:38:02 -0700 Subject: [PATCH] Batch stale review-flag writes and fix review cleanup ordering Move in_review flag set before try block so it is always visible, and consolidate per-session state writes into a single batch to avoid repeated disk I/O during list. Also guard against missing status entries with fallback defaults. Co-Authored-By: Claude Opus 4.6 --- src/commands/list.ts | 34 ++++++++++++++++++++++------------ src/commands/review.ts | 13 +++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index a87b647..bb24f96 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -33,25 +33,23 @@ export async function action(opts: { json?: boolean }) { if (sessions.length === 0) { if (opts.json) { console.log("[]") - return - } - console.log("◆ No active sessions.") - if ((await vm.status()) !== "running") { - console.log(`\n${red}VM is not running.${reset}`) + } else { + console.log("◆ No active sessions.") + if ((await vm.status()) !== "running") { + console.log(`\n${red}VM is not running.${reset}`) + } } return } // 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"] - // Self-heal: clear stale in_review flag if Claude is not active - if (!active && s.in_review) { - s.in_review = false - await state.setSession(root, s).catch(() => {}) - } + // Collect stale in_review flags for batch self-heal below + if (!active && s.in_review) staleReviewSessions.push(s) if (active) return [s.branch, "active"] const dirty = await git.isDirty(s.worktree) if (dirty) return [s.branch, "dirty"] @@ -61,6 +59,18 @@ export async function action(opts: { json?: boolean }) { ) const statuses = Object.fromEntries(statusEntries) + // Batch self-heal stale in_review flags with a single state write + if (staleReviewSessions.length > 0 && !opts.json) { + const current = await state.load(root).catch(() => null) + if (current) { + for (const s of staleReviewSessions) { + s.in_review = false + if (current.sessions[s.branch]) current.sessions[s.branch].in_review = false + } + await state.save(root, current).catch(() => {}) + } + } + if (opts.json) { const withStatus = sessions.map(s => ({ ...s, status: statuses[s.branch] })) console.log(JSON.stringify(withStatus, null, 2)) @@ -82,8 +92,8 @@ export async function action(opts: { json?: boolean }) { for (const s of sessions) { const prompt = (s.prompt ?? "").split("\n")[0] - const status = statuses[s.branch] - const { icon, color: bc } = styles[status] + const status = statuses[s.branch] ?? "idle" + const { icon, color: bc } = styles[status] ?? styles.idle const maxPrompt = cols - prefixWidth const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`) diff --git a/src/commands/review.ts b/src/commands/review.ts index 4600e5d..7face00 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -68,11 +68,10 @@ Your thoughts, in brief. ` if (extra) prompt += "\n\n" + extra - try { - // Mark session as in review inside try so finally always clears it - session.in_review = true - await state.setSession(root, session) + session.in_review = true + await state.setSession(root, session) + try { if (opts.print) { spin.text = "Running review…" const result = await vm.claude(session.worktree, { print: prompt }) @@ -82,11 +81,9 @@ Your thoughts, in brief. spin.succeed("Session ready") await vm.claude(session.worktree, { prompt }) } - - await saveChanges(session.worktree, session.branch) } finally { - // Clear review state — use .catch() per error handling conventions - // so a disk error here doesn't mask the original exception + // Always attempt save and clear review state + await saveChanges(session.worktree, session.branch).catch(() => {}) session.in_review = false await state.setSession(root, session).catch(() => {}) }