diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index a1a072dc3..e59e34146 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -585,10 +585,11 @@ export function updateProgressWidget( lines.push(rightAlign(headerLeft, headerRight, width)); // Worktree/branch right-aligned below header - if (worktreeName && cachedBranch) { - lines.push(rightAlign("", theme.fg("dim", `${worktreeName} (${cachedBranch})`), width)); - } else if (cachedBranch) { - lines.push(rightAlign("", theme.fg("dim", cachedBranch), width)); + const branchLabel = worktreeName && cachedBranch + ? `${worktreeName} (${cachedBranch})` + : cachedBranch ?? ""; + if (branchLabel) { + lines.push(rightAlign("", theme.fg("dim", branchLabel), width)); } // Show health signal details when degraded (yellow/red) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index e7558e57c..f21f6330d 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -895,6 +895,7 @@ export async function pauseAuto( sessionFile: s.pausedSessionFile, activeEngineId: s.activeEngineId, activeRunDir: s.activeRunDir, + autoStartTime: s.autoStartTime, }; const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime"); mkdirSync(runtimeDir, { recursive: true }); @@ -1137,6 +1138,7 @@ export async function startAuto( s.activeRunDir = meta.activeRunDir ?? null; s.originalBasePath = meta.originalBasePath || base; s.stepMode = meta.stepMode ?? requestedStepMode; + s.autoStartTime = meta.autoStartTime || Date.now(); s.paused = true; try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */ logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" }); @@ -1162,6 +1164,7 @@ export async function startAuto( s.currentMilestoneId = meta.milestoneId; s.originalBasePath = meta.originalBasePath || base; s.stepMode = meta.stepMode ?? requestedStepMode; + s.autoStartTime = meta.autoStartTime || Date.now(); s.paused = true; // Clean up the persisted file — we're consuming it try { unlinkSync(pausedPath); } catch (err) { /* non-fatal */ @@ -1194,6 +1197,7 @@ export async function startAuto( s.cmdCtx = ctx; s.basePath = base; setLogBasePath(base); + if (!s.autoStartTime || s.autoStartTime <= 0) s.autoStartTime = Date.now(); s.unitDispatchCount.clear(); s.unitLifetimeDispatches.clear(); if (!getLedger()) initMetrics(base); diff --git a/src/resources/extensions/gsd/tests/auto-start-time-persistence.test.ts b/src/resources/extensions/gsd/tests/auto-start-time-persistence.test.ts new file mode 100644 index 000000000..174a9b651 --- /dev/null +++ b/src/resources/extensions/gsd/tests/auto-start-time-persistence.test.ts @@ -0,0 +1,50 @@ +// GSD2 — Verify autoStartTime is persisted in paused-session.json and restored on resume +// Copyright (c) 2026 Jeremy McSpadden + +/** + * auto-start-time-persistence.test.ts — Ensures autoStartTime survives + * cross-session resume via paused-session.json (#3585). + * + * Source-code regression guards: verify auto.ts saves and restores + * autoStartTime so the elapsed timer doesn't vanish after /exit + resume. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const AUTO_TS_PATH = join(__dirname, "..", "auto.ts"); + +const source = readFileSync(AUTO_TS_PATH, "utf-8"); + +test("pauseAuto persists autoStartTime in paused-session.json (#3585)", () => { + assert.ok( + source.includes("autoStartTime: s.autoStartTime"), + "pausedMeta must include autoStartTime so the timer survives /exit", + ); +}); + +test("cross-session resume restores autoStartTime from paused-session.json (#3585)", () => { + const matches = source.match(/s\.autoStartTime\s*=\s*meta\.autoStartTime/g); + assert.ok( + matches && matches.length >= 2, + "both resume paths (custom workflow + milestone) must restore autoStartTime from meta", + ); +}); + +test("resume path falls back to Date.now() when autoStartTime is missing (#3585)", () => { + assert.ok( + source.includes("meta.autoStartTime || Date.now()"), + "restore should fall back to Date.now() for old paused-session files without autoStartTime", + ); +}); + +test("resume path guards against zero autoStartTime (#3585)", () => { + assert.ok( + source.includes("if (!s.autoStartTime || s.autoStartTime <= 0) s.autoStartTime = Date.now()"), + "resume path must set autoStartTime to Date.now() if still zero after restore", + ); +});