Extract resolveAllStatuses helper and fix stale review detection
Move stale review flag outside the vm-running block so sessions marked in_review are caught even when the VM is stopped. Extract shared status-resolution logic into resolveAllStatuses to deduplicate the list and list-all commands. Add stale lock detection to prevent deadlocks from crashed processes, and include status in JSON output for list-all. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f0b2a55c9c
commit
e2a76a6ad9
|
|
@ -50,9 +50,9 @@ async function resolveStatus(
|
||||||
if (vmRunning) {
|
if (vmRunning) {
|
||||||
const active = await vm.isClaudeActive(s.worktree, s.branch)
|
const active = await vm.isClaudeActive(s.worktree, s.branch)
|
||||||
if (active && s.in_review) return { key, status: "review", staleReview: false }
|
if (active && s.in_review) return { key, status: "review", staleReview: false }
|
||||||
staleReview = !active && !!s.in_review
|
|
||||||
if (active) return { key, status: "active", staleReview: false }
|
if (active) return { key, status: "active", staleReview: false }
|
||||||
}
|
}
|
||||||
|
staleReview = !!s.in_review
|
||||||
const dirty = await git.isDirty(s.worktree)
|
const dirty = await git.isDirty(s.worktree)
|
||||||
if (dirty) return { key, status: "dirty", staleReview }
|
if (dirty) return { key, status: "dirty", staleReview }
|
||||||
const commits = await git.hasNewCommits(s.worktree)
|
const commits = await git.hasNewCommits(s.worktree)
|
||||||
|
|
@ -71,6 +71,25 @@ async function clearStaleReviews(staleReviews: { repoRoot: string; branch: strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveAllStatuses<T extends { branch: string; worktree: string; in_review?: boolean }>(
|
||||||
|
sessions: T[],
|
||||||
|
vmRunning: boolean,
|
||||||
|
keyFn: (s: T) => string,
|
||||||
|
repoRootFn: (s: T) => string,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const results = await Promise.all(sessions.map(s => resolveStatus(s, keyFn(s), vmRunning)))
|
||||||
|
const statuses = Object.fromEntries(results.map(r => [r.key, r.status]))
|
||||||
|
const staleReviews: { repoRoot: string; branch: string }[] = []
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
if (results[i].staleReview) {
|
||||||
|
sessions[i].in_review = false
|
||||||
|
staleReviews.push({ repoRoot: repoRootFn(sessions[i]), branch: sessions[i].branch })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await clearStaleReviews(staleReviews)
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
async function backfillPrompts(sessions: { worktree: string; prompt?: string }[], vmRunning: boolean) {
|
async function backfillPrompts(sessions: { worktree: string; prompt?: string }[], vmRunning: boolean) {
|
||||||
if (!vmRunning) return
|
if (!vmRunning) return
|
||||||
const needsPrompt = sessions.filter(s => !s.prompt)
|
const needsPrompt = sessions.filter(s => !s.prompt)
|
||||||
|
|
@ -113,19 +132,7 @@ export async function action(opts: { json?: boolean; all?: boolean; add?: string
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const statuses = await resolveAllStatuses(sessions, vmRunning, s => s.branch, () => root)
|
||||||
sessions.map(s => resolveStatus(s, s.branch, vmRunning))
|
|
||||||
)
|
|
||||||
const statuses = Object.fromEntries(results.map(r => [r.key, r.status]))
|
|
||||||
|
|
||||||
const staleReviews: { repoRoot: string; branch: string }[] = []
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
|
||||||
if (results[i].staleReview) {
|
|
||||||
sessions[i].in_review = false
|
|
||||||
staleReviews.push({ repoRoot: root, branch: sessions[i].branch })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await clearStaleReviews(staleReviews)
|
|
||||||
|
|
||||||
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] }))
|
||||||
|
|
@ -165,12 +172,7 @@ async function actionRemove(dir: string) {
|
||||||
async function actionAll(opts: { json?: boolean }) {
|
async function actionAll(opts: { json?: boolean }) {
|
||||||
const sessions = await state.loadAll()
|
const sessions = await state.loadAll()
|
||||||
|
|
||||||
if (opts.json) {
|
if (sessions.length === 0 && !opts.json) {
|
||||||
console.log(JSON.stringify(sessions, null, 2))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
|
||||||
console.log("◆ No active sessions across any project.")
|
console.log("◆ No active sessions across any project.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -179,19 +181,15 @@ async function actionAll(opts: { json?: boolean }) {
|
||||||
|
|
||||||
await backfillPrompts(sessions, vmRunning)
|
await backfillPrompts(sessions, vmRunning)
|
||||||
|
|
||||||
const results = await Promise.all(
|
const statuses = await resolveAllStatuses(
|
||||||
sessions.map(s => resolveStatus(s, `${s.repo}/${s.branch}`, vmRunning))
|
sessions, vmRunning, s => `${s.repo}/${s.branch}`, s => s.repoRoot,
|
||||||
)
|
)
|
||||||
const statuses = Object.fromEntries(results.map(r => [r.key, r.status]))
|
|
||||||
|
|
||||||
const staleReviews: { repoRoot: string; branch: string }[] = []
|
if (opts.json) {
|
||||||
for (let i = 0; i < results.length; i++) {
|
const withStatus = sessions.map(s => ({ ...s, status: statuses[`${s.repo}/${s.branch}`] }))
|
||||||
if (results[i].staleReview) {
|
console.log(JSON.stringify(withStatus, null, 2))
|
||||||
sessions[i].in_review = false
|
return
|
||||||
staleReviews.push({ repoRoot: sessions[i].repoRoot, branch: sessions[i].branch })
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
await clearStaleReviews(staleReviews)
|
|
||||||
|
|
||||||
const byRepo = Map.groupBy(sessions, s => s.repo)
|
const byRepo = Map.groupBy(sessions, s => s.repo)
|
||||||
|
|
||||||
|
|
|
||||||
22
src/state.ts
22
src/state.ts
|
|
@ -1,5 +1,5 @@
|
||||||
import { join, basename, resolve } from "path"
|
import { join, basename, resolve } from "path"
|
||||||
import { readdir, rename, mkdir, rmdir } from "fs/promises"
|
import { readdir, rename, mkdir, rmdir, stat } from "fs/promises"
|
||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
|
|
@ -71,6 +71,14 @@ async function withGlobalLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
await mkdir(lockPath)
|
await mkdir(lockPath)
|
||||||
break
|
break
|
||||||
} catch {
|
} catch {
|
||||||
|
// If the lock is older than 30 seconds, assume it's stale (crashed process)
|
||||||
|
try {
|
||||||
|
const info = await stat(lockPath)
|
||||||
|
if (Date.now() - info.mtimeMs > 30_000) {
|
||||||
|
await rmdir(lockPath).catch(() => {})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
if (i === 19) throw new Error("Could not acquire registry lock")
|
if (i === 19) throw new Error("Could not acquire registry lock")
|
||||||
await Bun.sleep(50)
|
await Bun.sleep(50)
|
||||||
}
|
}
|
||||||
|
|
@ -159,18 +167,8 @@ export async function scanAndRegister(dir: string, maxDepth = 5): Promise<string
|
||||||
|
|
||||||
await walk(root, 0)
|
await walk(root, 0)
|
||||||
|
|
||||||
if (found.length > 0) {
|
|
||||||
await withGlobalLock(async () => {
|
|
||||||
const gs = await loadGlobal()
|
|
||||||
let changed = false
|
|
||||||
for (const p of found) {
|
for (const p of found) {
|
||||||
if (!gs.projects.includes(p)) {
|
await registerProject(p)
|
||||||
gs.projects.push(p)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed) await saveGlobal(gs)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return found
|
return found
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user