From 38c300fd80f09ec2589bb4f24ae71317df1b42cc Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Mon, 16 Mar 2026 12:35:03 -0600 Subject: [PATCH] fix: reuse crash recovery infrastructure for pause/resume context (#665) Capture the session file path on pause and synthesize a recovery briefing on resume, so the next agent knows what already happened instead of restarting the unit from scratch. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto.ts | 31 +++++++++++++++++++ .../extensions/gsd/session-forensics.ts | 10 +++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index cc3cd5a15..b9058d6e6 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -244,6 +244,9 @@ function escapeStaleWorktree(base: string): string { /** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */ let pendingCrashRecovery: string | null = null; +/** Session file path captured at pause — used to synthesize recovery briefing on resume */ +let pausedSessionFile: string | null = null; + /** Dashboard tracking */ let autoStartTime: number = 0; let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = []; @@ -588,6 +591,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi clearActivityLogState(); resetProactiveHealing(); pendingCrashRecovery = null; + pausedSessionFile = null; _handlingAgentEnd = false; ctx?.ui.setStatus("gsd-auto", undefined); ctx?.ui.setWidget("gsd-progress", undefined); @@ -612,6 +616,11 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Promise { if (!active) return; clearUnitTimeout(); + + // Capture the current session file before clearing state — used for + // recovery briefing on resume so the next agent knows what already happened. + pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null; + if (lockBase()) clearLock(lockBase()); // Remove SIGTERM handler registered at auto-mode start @@ -709,6 +718,28 @@ export async function startAuto( // Self-heal: clear stale runtime records where artifacts already exist await selfHealRuntimeRecords(basePath, ctx, completedKeySet); invalidateAllCaches(); + + // Synthesize recovery briefing from the paused session so the next agent + // knows what already happened (reuses crash recovery infrastructure). + if (pausedSessionFile) { + const activityDir = join(gsdRoot(basePath), "activity"); + const recovery = synthesizeCrashRecovery( + basePath, + currentUnit?.type ?? "unknown", + currentUnit?.id ?? "unknown", + pausedSessionFile, + activityDir, + ); + if (recovery && recovery.trace.toolCallCount > 0) { + pendingCrashRecovery = recovery.prompt; + ctx.ui.notify( + `Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`, + "info", + ); + } + pausedSessionFile = null; + } + await dispatchNextUnit(ctx, pi); return; } diff --git a/src/resources/extensions/gsd/session-forensics.ts b/src/resources/extensions/gsd/session-forensics.ts index d7c34bb95..4abf760a2 100644 --- a/src/resources/extensions/gsd/session-forensics.ts +++ b/src/resources/extensions/gsd/session-forensics.ts @@ -313,10 +313,10 @@ function formatRecoveryPrompt( const sections: string[] = []; sections.push( - "## Crash Recovery Briefing", + "## Recovery Briefing", "", - `You are resuming \`${unitType}\` for \`${unitId}\` after a crash.`, - `The previous session completed **${trace.toolCallCount} tool calls** before dying.`, + `You are resuming \`${unitType}\` for \`${unitId}\` after an interruption.`, + `The previous session completed **${trace.toolCallCount} tool calls** before stopping.`, "Use this briefing to pick up exactly where it left off. Do NOT redo completed work.", ); @@ -352,7 +352,7 @@ function formatRecoveryPrompt( // Errors if (trace.errors.length > 0) { sections.push( - "", "### Errors Before Crash", + "", "### Errors Before Interruption", ...trace.errors.slice(-3).map(e => `- ${truncate(e, 200)}`), ); } @@ -368,7 +368,7 @@ function formatRecoveryPrompt( // Last reasoning if (trace.lastReasoning) { sections.push( - "", "### Last Agent Reasoning Before Crash", + "", "### Last Agent Reasoning Before Interruption", `> ${trace.lastReasoning.replace(/\n/g, "\n> ")}`, ); }