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 <noreply@anthropic.com>
This commit is contained in:
Chris Wanstrath 2026-03-18 23:38:02 -07:00
parent 3ba550d80a
commit a682539db3
2 changed files with 27 additions and 20 deletions

View File

@ -33,25 +33,23 @@ export async function action(opts: { json?: boolean }) {
if (sessions.length === 0) { if (sessions.length === 0) {
if (opts.json) { if (opts.json) {
console.log("[]") console.log("[]")
return } else {
} console.log("◆ No active sessions.")
console.log("◆ No active sessions.") if ((await vm.status()) !== "running") {
if ((await vm.status()) !== "running") { console.log(`\n${red}VM is not running.${reset}`)
console.log(`\n${red}VM is not running.${reset}`) }
} }
return return
} }
// Determine status for each session in parallel // Determine status for each session in parallel
const staleReviewSessions: state.Session[] = []
const statusEntries = await Promise.all( const statusEntries = await Promise.all(
sessions.map(async (s): Promise<[string, string]> => { sessions.map(async (s): Promise<[string, string]> => {
const active = await vm.isClaudeActive(s.worktree, s.branch) const active = await vm.isClaudeActive(s.worktree, s.branch)
if (active && s.in_review) return [s.branch, "review"] if (active && s.in_review) return [s.branch, "review"]
// Self-heal: clear stale in_review flag if Claude is not active // Collect stale in_review flags for batch self-heal below
if (!active && s.in_review) { if (!active && s.in_review) staleReviewSessions.push(s)
s.in_review = false
await state.setSession(root, s).catch(() => {})
}
if (active) return [s.branch, "active"] if (active) return [s.branch, "active"]
const dirty = await git.isDirty(s.worktree) const dirty = await git.isDirty(s.worktree)
if (dirty) return [s.branch, "dirty"] if (dirty) return [s.branch, "dirty"]
@ -61,6 +59,18 @@ export async function action(opts: { json?: boolean }) {
) )
const statuses = Object.fromEntries(statusEntries) 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) { if (opts.json) {
const withStatus = sessions.map(s => ({ ...s, status: statuses[s.branch] })) const withStatus = sessions.map(s => ({ ...s, status: statuses[s.branch] }))
console.log(JSON.stringify(withStatus, null, 2)) console.log(JSON.stringify(withStatus, null, 2))
@ -82,8 +92,8 @@ export async function action(opts: { json?: boolean }) {
for (const s of sessions) { for (const s of sessions) {
const prompt = (s.prompt ?? "").split("\n")[0] const prompt = (s.prompt ?? "").split("\n")[0]
const status = statuses[s.branch] const status = statuses[s.branch] ?? "idle"
const { icon, color: bc } = styles[status] const { icon, color: bc } = styles[status] ?? styles.idle
const maxPrompt = cols - prefixWidth const maxPrompt = cols - prefixWidth
const truncated = maxPrompt > 3 && prompt.length > maxPrompt ? prompt.slice(0, maxPrompt - 3) + "..." : prompt 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}`) console.log(`${icon} ${bc}${s.branch.padEnd(branchWidth)}${reset} ${dim}${truncated}${reset}`)

View File

@ -68,11 +68,10 @@ Your thoughts, in brief.
` `
if (extra) prompt += "\n\n" + extra if (extra) prompt += "\n\n" + extra
try { session.in_review = true
// Mark session as in review inside try so finally always clears it await state.setSession(root, session)
session.in_review = true
await state.setSession(root, session)
try {
if (opts.print) { if (opts.print) {
spin.text = "Running review…" spin.text = "Running review…"
const result = await vm.claude(session.worktree, { print: prompt }) const result = await vm.claude(session.worktree, { print: prompt })
@ -82,11 +81,9 @@ Your thoughts, in brief.
spin.succeed("Session ready") spin.succeed("Session ready")
await vm.claude(session.worktree, { prompt }) await vm.claude(session.worktree, { prompt })
} }
await saveChanges(session.worktree, session.branch)
} finally { } finally {
// Clear review state — use .catch() per error handling conventions // Always attempt save and clear review state
// so a disk error here doesn't mask the original exception await saveChanges(session.worktree, session.branch).catch(() => {})
session.in_review = false session.in_review = false
await state.setSession(root, session).catch(() => {}) await state.setSession(root, session).catch(() => {})
} }