fix: call selfHealRuntimeRecords before autoLoop to clear orphaned dispatched records (#1772)

When auto-mode dies after a subagent completes but before agent_end is
processed, the runtime record stays permanently at "phase": "dispatched"
with no recovery path. selfHealRuntimeRecords was only called from the
manual guided-flow wizard, never from auto-loop startup.

Add selfHealRuntimeRecords(basePath, ctx) before both autoLoop call
sites in startAuto (resume path and fresh-start path) so stale
dispatched records are cleared on every auto-mode entry.

Closes #1727

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-21 09:32:59 -06:00 committed by GitHub
parent fe63ccad10
commit b609c3b30b
2 changed files with 33 additions and 0 deletions

View file

@ -115,6 +115,7 @@ import {
formatHealthSummary,
getConsecutiveErrorUnits,
} from "./doctor-proactive.js";
import { selfHealRuntimeRecords } from "./auto-recovery.js";
import { clearSkillSnapshot } from "./skill-discovery.js";
import {
captureAvailableSkills,
@ -1052,6 +1053,9 @@ export async function startAuto(
);
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
// Clear orphaned runtime records from prior process deaths before entering the loop
await selfHealRuntimeRecords(s.basePath, ctx);
await autoLoop(ctx, pi, s, buildLoopDeps());
return;
}
@ -1082,6 +1086,9 @@ export async function startAuto(
}
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
// Clear orphaned runtime records from prior process deaths before entering the loop
await selfHealRuntimeRecords(s.basePath, ctx);
// Dispatch the first unit
await autoLoop(ctx, pi, s, buildLoopDeps());
}

View file

@ -1098,6 +1098,32 @@ test("auto.ts startAuto calls autoLoop (not dispatchNextUnit as first dispatch)"
);
});
test("startAuto calls selfHealRuntimeRecords before autoLoop (#1727)", () => {
const src = readFileSync(
resolve(import.meta.dirname, "..", "auto.ts"),
"utf-8",
);
const fnIdx = src.indexOf("export async function startAuto");
assert.ok(fnIdx > -1, "startAuto must exist in auto.ts");
const fnEnd = src.indexOf("\n// ─── ", fnIdx + 100);
const fnBlock =
fnEnd > -1 ? src.slice(fnIdx, fnEnd) : src.slice(fnIdx, fnIdx + 5000);
// Both autoLoop call sites must be preceded by selfHealRuntimeRecords
const healIdx = fnBlock.indexOf("selfHealRuntimeRecords");
const loopIdx = fnBlock.indexOf("autoLoop(");
assert.ok(healIdx > -1, "startAuto must call selfHealRuntimeRecords");
assert.ok(healIdx < loopIdx, "selfHealRuntimeRecords must be called before autoLoop");
// Verify the second autoLoop call site also has selfHeal before it
const secondLoopIdx = fnBlock.indexOf("autoLoop(", loopIdx + 1);
if (secondLoopIdx > -1) {
const secondHealIdx = fnBlock.indexOf("selfHealRuntimeRecords", healIdx + 1);
assert.ok(secondHealIdx > -1, "second autoLoop call must also have selfHealRuntimeRecords");
assert.ok(secondHealIdx < secondLoopIdx, "second selfHealRuntimeRecords must precede second autoLoop");
}
});
test("agent_end handler calls resolveAgentEnd (not handleAgentEnd)", () => {
const hooksSrc = readFileSync(
resolve(import.meta.dirname, "..", "bootstrap", "register-hooks.ts"),