fix(auto): avoid resuming blocked no-unit sessions

This commit is contained in:
Mikael Hugo 2026-05-15 20:56:15 +02:00
parent 03f6d4990f
commit ecf6af92e8
3 changed files with 58 additions and 25 deletions

View file

@ -1208,30 +1208,34 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
// Persist paused-session metadata so resume survives /exit (#1383).
// The fresh-start bootstrap checks for this file and restores worktree context.
try {
const pausedMeta = {
milestoneId: s.currentMilestoneId,
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
originalBasePath: s.originalBasePath,
stepMode: s.stepMode,
pausedAt: new Date().toISOString(),
sessionFile: s.pausedSessionFile,
unitType: s.currentUnit?.type ?? undefined,
unitId: s.currentUnit?.id ?? undefined,
activeEngineId: s.activeEngineId,
activeRunDir: s.activeRunDir,
autoStartTime: s.autoStartTime,
milestoneLock: s.sessionMilestoneLock ?? undefined,
};
const runtimeDir = join(
sfRoot(s.originalBasePath || s.basePath),
"runtime",
);
mkdirSync(runtimeDir, { recursive: true });
writeFileSync(
join(runtimeDir, "paused-session.json"),
JSON.stringify(pausedMeta, null, 2),
"utf-8",
);
const hasResumableUnit = !!(s.currentUnit?.type && s.currentUnit?.id);
const hasResumableCustomEngine = !!s.activeEngineId && s.activeEngineId !== "dev";
if (hasResumableUnit || hasResumableCustomEngine) {
const pausedMeta = {
milestoneId: s.currentMilestoneId,
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
originalBasePath: s.originalBasePath,
stepMode: s.stepMode,
pausedAt: new Date().toISOString(),
sessionFile: s.pausedSessionFile,
unitType: s.currentUnit?.type ?? undefined,
unitId: s.currentUnit?.id ?? undefined,
activeEngineId: s.activeEngineId,
activeRunDir: s.activeRunDir,
autoStartTime: s.autoStartTime,
milestoneLock: s.sessionMilestoneLock ?? undefined,
};
const runtimeDir = join(
sfRoot(s.originalBasePath || s.basePath),
"runtime",
);
mkdirSync(runtimeDir, { recursive: true });
writeFileSync(
join(runtimeDir, "paused-session.json"),
JSON.stringify(pausedMeta, null, 2),
"utf-8",
);
}
} catch (err) {
// Non-fatal — resume will still work via full bootstrap, just without worktree context
logWarning(

View file

@ -26,7 +26,9 @@ export function isBootstrapCrashLock(lock) {
);
}
export function hasResumableDerivedState(state) {
return !!(state?.activeMilestone && state.phase !== "complete");
if (!state?.activeMilestone) return false;
if (state.phase === "complete" || state.phase === "blocked") return false;
return true;
}
export async function assessInterruptedSession(basePath) {
const pausedSession = readPausedSessionMetadata(basePath);

View file

@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { hasResumableDerivedState } from "../interrupted-session.js";
test("hasResumableDerivedState_rejects_blocked_state_without_active_work", () => {
assert.equal(
hasResumableDerivedState({
activeMilestone: { id: "M005", title: "Blocked milestone" },
activeSlice: null,
activeTask: null,
phase: "blocked",
}),
false,
);
});
test("hasResumableDerivedState_accepts_active_non_blocked_state", () => {
assert.equal(
hasResumableDerivedState({
activeMilestone: { id: "M005", title: "Active milestone" },
activeSlice: { id: "S01", title: "Active slice" },
activeTask: null,
phase: "planning",
}),
true,
);
});