Merge pull request #671 from gsd-build/fix/665-auto-resume-recovery

fix: auto-mode resume preserves context from paused session
This commit is contained in:
TÂCHES 2026-03-16 12:44:08 -06:00 committed by GitHub
commit 0a43e6de4e
2 changed files with 36 additions and 5 deletions

View file

@ -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<void> {
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;
}

View file

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