diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 9d3cda351..1b15b38dc 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -1023,6 +1023,12 @@ export async function startAuto( s.stepMode = s.stepMode || requestedStepMode; s.cmdCtx = ctx; s.basePath = base; + s.autoStartTime = Date.now(); + s.originalModelId = ctx.model?.id ?? null; + s.originalModelProvider = ctx.model?.provider ?? null; + if (ctx.model) { + s.autoModeStartModel = { provider: ctx.model.provider, id: ctx.model.id }; + } s.unitDispatchCount.clear(); s.unitLifetimeDispatches.clear(); if (!getLedger()) initMetrics(base); diff --git a/src/resources/extensions/gsd/interrupted-session.ts b/src/resources/extensions/gsd/interrupted-session.ts index f226c9507..e5f6dcc2e 100644 --- a/src/resources/extensions/gsd/interrupted-session.ts +++ b/src/resources/extensions/gsd/interrupted-session.ts @@ -74,7 +74,10 @@ export async function assessInterruptedSession( basePath: string, ): Promise { const pausedSession = readPausedSessionMetadata(basePath); - const assessmentBasePath = pausedSession?.worktreePath || basePath; + const worktreeExists = pausedSession?.worktreePath + ? existsSync(pausedSession.worktreePath) + : false; + const assessmentBasePath = worktreeExists ? pausedSession!.worktreePath! : basePath; const rawLock = readCrashLock(basePath); const lock = rawLock && rawLock.pid !== process.pid ? rawLock : null; diff --git a/src/resources/extensions/gsd/tests/crash-recovery.test.ts b/src/resources/extensions/gsd/tests/crash-recovery.test.ts index c396ee728..4055a371b 100644 --- a/src/resources/extensions/gsd/tests/crash-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/crash-recovery.test.ts @@ -251,6 +251,39 @@ test("assessInterruptedSession marks stale paused-session metadata as stale when } }); +test("assessInterruptedSession classifies paused session without lock as recoverable when disk state is resumable", async () => { + const base = makeTmpBase(); + try { + writeRoadmap(base, false); + writePausedSession(base, "M001", true); + + const assessment = await assessInterruptedSession(base); + assert.equal(assessment.classification, "recoverable"); + assert.equal(assessment.lock, null); + assert.equal(assessment.pausedSession?.milestoneId, "M001"); + assert.equal(assessment.hasResumableDiskState, true); + assert.equal(assessment.isBootstrapCrash, false); + } finally { + cleanup(base); + } +}); + +test("assessInterruptedSession falls back to basePath when worktreePath no longer exists", async () => { + const base = makeTmpBase(); + try { + writeRoadmap(base, false); + // Reference a worktree that doesn't exist on disk + writePausedSession(base, "M001", false, "/nonexistent/worktree"); + + const assessment = await assessInterruptedSession(base); + // Should use basePath (which has an unfinished roadmap) instead of the missing worktree + assert.equal(assessment.classification, "recoverable"); + assert.equal(assessment.hasResumableDiskState, true); + } finally { + cleanup(base); + } +}); + test("assessInterruptedSession prefers paused worktree state when worktreePath is recorded", async () => { const base = makeTmpBase(); const worktree = join(base, "worktree-copy");