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 (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}`)

View File

@ -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(() => {})
}