From 690bcbd79c16579067bc7ac3b13153b7bde17e01 Mon Sep 17 00:00:00 2001 From: Derek Pearson Date: Sat, 21 Mar 2026 21:27:44 -0400 Subject: [PATCH] fix(gsd): address QA round 3 - Set resourceVersionOnStart on paused-session resume so resource staleness detection works for resumed sessions. - Re-register setLevelChangeCallback on resume so health-level transition notifications fire after process restart. - Persist unitType/unitId in paused-session metadata and restore them on resume so recovery synthesis framing text shows the actual unit instead of "unknown"/"unknown". - Check worktreePath existence before showing "(worktree)" in resume notification to avoid misleading the user when the worktree was already torn down. - Fix showDiscuss skip_milestone to use step:false (matching discuss_draft and discuss_fresh) instead of hardcoded step:true. --- src/resources/extensions/gsd/auto.ts | 17 ++++++++++++++--- src/resources/extensions/gsd/auto/session.ts | 4 ++++ src/resources/extensions/gsd/guided-flow.ts | 2 +- .../extensions/gsd/interrupted-session.ts | 2 ++ .../gsd/tests/interrupted-session-ui.test.ts | 2 +- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 1b15b38dc..fa05b6cb9 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -731,6 +731,8 @@ export async function pauseAuto( stepMode: s.stepMode, pausedAt: new Date().toISOString(), sessionFile: s.pausedSessionFile, + unitType: s.currentUnit?.type ?? undefined, + unitId: s.currentUnit?.id ?? undefined, }; const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime"); mkdirSync(runtimeDir, { recursive: true }); @@ -968,10 +970,12 @@ export async function startAuto( s.originalBasePath = meta.originalBasePath || base; s.stepMode = meta.stepMode ?? requestedStepMode; s.pausedSessionFile = meta.sessionFile ?? null; + s.pausedUnitType = meta.unitType ?? null; + s.pausedUnitId = meta.unitId ?? null; s.paused = true; try { unlinkSync(pausedPath); } catch { /* non-fatal */ } ctx.ui.notify( - `Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, + `Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`, "info", ); } else if (existsSync(pausedPath)) { @@ -1024,6 +1028,7 @@ export async function startAuto( s.cmdCtx = ctx; s.basePath = base; s.autoStartTime = Date.now(); + s.resourceVersionOnStart = readResourceVersion(); s.originalModelId = ctx.model?.id ?? null; s.originalModelProvider = ctx.model?.provider ?? null; if (ctx.model) { @@ -1034,6 +1039,12 @@ export async function startAuto( if (!getLedger()) initMetrics(base); if (s.currentMilestoneId) setActiveMilestoneId(base, s.currentMilestoneId); + // Re-register health level notification callback lost across process restart + setLevelChangeCallback((_from, to, summary) => { + const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info"; + ctx.ui.notify(summary, level as "info" | "warning" | "error"); + }); + // ── Auto-worktree: re-enter worktree on resume ── if ( s.currentMilestoneId && @@ -1084,8 +1095,8 @@ export async function startAuto( const activityDir = join(gsdRoot(s.basePath), "activity"); const recovery = synthesizeCrashRecovery( s.basePath, - s.currentUnit?.type ?? "unknown", - s.currentUnit?.id ?? "unknown", + s.currentUnit?.type ?? s.pausedUnitType ?? "unknown", + s.currentUnit?.id ?? s.pausedUnitId ?? "unknown", s.pausedSessionFile ?? undefined, activityDir, ); diff --git a/src/resources/extensions/gsd/auto/session.ts b/src/resources/extensions/gsd/auto/session.ts index 016a7fdf6..8ffffdd2f 100644 --- a/src/resources/extensions/gsd/auto/session.ts +++ b/src/resources/extensions/gsd/auto/session.ts @@ -118,6 +118,8 @@ export class AutoSession { pendingVerificationRetry: PendingVerificationRetry | null = null; readonly verificationRetryCount = new Map(); pausedSessionFile: string | null = null; + pausedUnitType: string | null = null; + pausedUnitId: string | null = null; resourceVersionOnStart: string | null = null; lastStateRebuildAt = 0; @@ -203,6 +205,8 @@ export class AutoSession { this.pendingVerificationRetry = null; this.verificationRetryCount.clear(); this.pausedSessionFile = null; + this.pausedUnitType = null; + this.pausedUnitId = null; this.resourceVersionOnStart = null; this.lastStateRebuildAt = 0; diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 04073b611..6f1b378f5 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -558,7 +558,7 @@ export async function showDiscuss( const milestoneIds = findMilestoneIds(basePath); const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids; const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds); - pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: true }; + pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false }; await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone"); } return; diff --git a/src/resources/extensions/gsd/interrupted-session.ts b/src/resources/extensions/gsd/interrupted-session.ts index e5f6dcc2e..21d347133 100644 --- a/src/resources/extensions/gsd/interrupted-session.ts +++ b/src/resources/extensions/gsd/interrupted-session.ts @@ -29,6 +29,8 @@ export interface PausedSessionMetadata { stepMode?: boolean; pausedAt?: string; sessionFile?: string | null; + unitType?: string; + unitId?: string; } export interface InterruptedSessionAssessment { diff --git a/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts b/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts index 6a2d0f39d..a97f54e06 100644 --- a/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +++ b/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts @@ -134,5 +134,5 @@ test("guided-flow source uses step-aware resume and clears stale paused metadata assert.ok(source.includes('step: interrupted.pausedSession?.stepMode ?? false')); assert.ok(source.includes('unlinkSync(join(gsdRoot(basePath), "runtime", "paused-session.json"))')); assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };')); - assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: true };')); + assert.ok(source.includes('pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };')); });