diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index 326175431..7279589bd 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -1136,31 +1136,74 @@ export async function autoLoop(ctx, pi, s, deps) { const midTitle = sfState.activeMilestone?.title ?? ""; const sliceId = sfState.activeSlice?.id ?? "reassess"; if (mid) { - ctx.ui.notify( - `Health issues detected with slice references — queuing reassess-roadmap instead of pausing.`, - "warning", - { - noticeKind: NOTICE_KIND.SYSTEM_NOTICE, - dedupe_key: "doctor-health-reassess-roadmap", - }, - ); - const { buildReassessRoadmapPrompt } = await import( - "../auto-prompts.js" - ); - const reassessPrompt = await buildReassessRoadmapPrompt( - mid, - midTitle, - sliceId, - s.basePath, - ); - s.sidecarQueue.unshift({ - kind: "hook", - unitType: "reassess-roadmap", - unitId: `${mid}/${sliceId}`, - prompt: `## Doctor Health Issues\n\n${healthCheck.issues.map((i) => `- ${i}`).join("\n")}\n\n${reassessPrompt}`, - }); - finishTurn("retry"); - continue; + // Convergence guard (Ralph Wiggum): if the SAME + // reassess-roadmap target just ran 3+ consecutive + // times the doctor's slice-ref issues evidently + // aren't being resolved by reassessment. Skip + // the redispatch, file self-feedback, and fall + // through to normal pre-dispatch so the existing + // detectStuck path (Rule 2) can break the loop + // instead of looping forever burning tokens. + const newKey = `reassess-roadmap:${mid}/${sliceId}`; + const recentKeys = (loopState.recentUnits || []) + .slice(-3) + .map((u) => u?.key); + const stuckOnReassess = + recentKeys.length === 3 && + recentKeys.every((k) => k === newKey); + if (stuckOnReassess) { + ctx.ui.notify( + `Convergence guard: ${newKey} succeeded 3 consecutive times but doctor's slice-ref issues persist. Skipping redispatch — running normal pre-dispatch so detectStuck can break the loop.`, + "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "convergence-guard-reassess", + }, + ); + try { + recordSelfFeedback( + { + kind: "engine-loop:non-converging-redispatch", + severity: "high", + summary: `${newKey} dispatched 3 consecutive times with success exit, but doctor's slice-reference health issues persist. Convergence guard skipped further redispatch.`, + evidence: `Doctor health issues persisting after 3 successful reassess-roadmap cycles: ${healthCheck.issues.slice(0, 5).join(" | ")}`, + }, + s.basePath, + ); + } catch { + // Filing must never block the loop's recovery path. + } + // Fall through to normal pre-dispatch (no + // unshift, no finishTurn — the next phases + // will either advance state via a different + // unit or hit detectStuck and bail. + } else { + ctx.ui.notify( + `Health issues detected with slice references — queuing reassess-roadmap instead of pausing.`, + "warning", + { + noticeKind: NOTICE_KIND.SYSTEM_NOTICE, + dedupe_key: "doctor-health-reassess-roadmap", + }, + ); + const { buildReassessRoadmapPrompt } = await import( + "../auto-prompts.js" + ); + const reassessPrompt = await buildReassessRoadmapPrompt( + mid, + midTitle, + sliceId, + s.basePath, + ); + s.sidecarQueue.unshift({ + kind: "hook", + unitType: "reassess-roadmap", + unitId: `${mid}/${sliceId}`, + prompt: `## Doctor Health Issues\n\n${healthCheck.issues.map((i) => `- ${i}`).join("\n")}\n\n${reassessPrompt}`, + }); + finishTurn("retry"); + continue; + } } } }