From a0a20599a0565638c0a52c7ca94a1b5328e42242 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 5 Apr 2026 21:04:05 -0500 Subject: [PATCH 1/2] fix(gsd): persist autoStartTime across session resume so elapsed timer survives /exit autoStartTime was never saved to paused-session.json, so cross-session resume always started with autoStartTime=0 and the widget showed no elapsed timer. Now saved on pause, restored on resume with Date.now() fallback for old files. Also fixes widget layout: elapsed/ETA stays on the header line above the milestone/branch info line. --- src/resources/extensions/gsd/auto-dashboard.ts | 9 +++++---- src/resources/extensions/gsd/auto.ts | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) 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); From b6794956f8e1ae3e8fe0ef4b102858511b930ee3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 5 Apr 2026 21:07:58 -0500 Subject: [PATCH 2/2] test(gsd): add regression tests for autoStartTime persistence (#3585) Source-code guards verifying autoStartTime is saved in paused-session.json, restored on both resume paths, and falls back to Date.now() when missing. --- .../tests/auto-start-time-persistence.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/auto-start-time-persistence.test.ts 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", + ); +});