diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index 5dc4e4fda..eec802386 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -671,8 +671,18 @@ export async function autoLoop(ctx, pi, s, deps) { iteration, elapsedMs, }); - // Do not break the loop — the watchdog only emits observability - // signals. The operator or a future gate can decide to stop. + // #wiggums: when stuck, back off instead of tight-spinning. The + // old comment said "Do not break the loop — observability only", + // but observability without backoff means we burn CPU writing + // halt-watchdog-break events at ~1Hz while whatever's actually + // stuck stays stuck. 2026-05-17 dogfood logged 60+ such events + // in a single 30s window. Backoff is exponential up to 30s, so + // short stalls recover quickly while long stalls don't melt the + // CPU. Heartbeat() will clear the backoff when real progress + // resumes. + const stuckCycles = Math.floor(elapsedMs / DEFAULT_HALT_THRESHOLD_MS); + const backoffMs = Math.min(30_000, 1000 * 2 ** Math.min(5, stuckCycles)); + await new Promise((resolve) => setTimeout(resolve, backoffMs)); } // ── Journal: per-iteration flow grouping ── const flowId = randomUUID();