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.
This commit is contained in:
Derek Pearson 2026-03-21 21:27:44 -04:00
parent 364cc7dcbc
commit 690bcbd79c
5 changed files with 22 additions and 5 deletions

View file

@ -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,
);

View file

@ -118,6 +118,8 @@ export class AutoSession {
pendingVerificationRetry: PendingVerificationRetry | null = null;
readonly verificationRetryCount = new Map<string, number>();
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;

View file

@ -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;

View file

@ -29,6 +29,8 @@ export interface PausedSessionMetadata {
stepMode?: boolean;
pausedAt?: string;
sessionFile?: string | null;
unitType?: string;
unitId?: string;
}
export interface InterruptedSessionAssessment {

View file

@ -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 };'));
});