diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index 637ae083c..8f5524df8 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -11,11 +11,14 @@ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { atomicWriteSync, delay } from "../atomic-write.js"; import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js"; +import { getTotalToolCallCount } from "../auto-tool-tracking.js"; import { runAutomaticAutonomousSolverEval } from "../autonomous-solver-eval.js"; import { debugLog } from "../debug-logger.js"; import { resolveEngine } from "../engine-resolver.js"; +import { getErrorMessage } from "../error-utils.js"; import { NOTICE_KIND } from "../notification-store.js"; import { sfRoot } from "../paths.js"; +import { recordSelfFeedback } from "../self-feedback.js"; import { getDatabase } from "../sf-db.js"; import { ExecutionGraphScheduler, @@ -40,8 +43,6 @@ import { } from "./phases.js"; import { _clearCurrentResolve } from "./resolve.js"; import { MAX_LOOP_ITERATIONS } from "./types.js"; -import { getErrorMessage } from "../error-utils.js"; -import { recordSelfFeedback } from "../self-feedback.js"; // ── Stuck detection persistence (#3704) ────────────────────────────────── // Persist stuck detection state to disk so it survives session restarts. @@ -325,7 +326,7 @@ async function drainSleeptimeQueue(basePath) { ? typeof result.response === "string" ? result.response : JSON.stringify(result.response) - : result.error ?? ""; + : (result.error ?? ""); db.prepare( `UPDATE sleeptime_consolidation_queue SET status = 'done', processed_at = :ts, result = :result @@ -593,7 +594,17 @@ export async function autoLoop(ctx, pi, s, deps) { const recentErrorMessages = []; const watchdog = new HaltWatchdog(s.basePath); watchdog.heartbeat(); // initial heartbeat before entering the loop + let lastObservedToolCallCount = getTotalToolCallCount(); + let unproductiveIterations = 0; while (s.active) { + const toolCallCount = getTotalToolCallCount(); + if (toolCallCount !== lastObservedToolCallCount) { + lastObservedToolCallCount = toolCallCount; + unproductiveIterations = 0; + watchdog.heartbeat(); + } else { + unproductiveIterations++; + } iteration++; debugLog("autoLoop", { phase: "loop-top", iteration }); // ── Halt watchdog: detect idle/stuck iterations ── @@ -644,22 +655,23 @@ export async function autoLoop(ctx, pi, s, deps) { basePath: s.basePath, startedAt: turnStartedAt, }); - if (iteration > MAX_LOOP_ITERATIONS) { + if (unproductiveIterations > MAX_LOOP_ITERATIONS) { debugLog("autoLoop", { phase: "exit", reason: "max-iterations", iteration, + unproductiveIterations, }); if (s.isYolo()) { logWarning( "dispatch", - `YOLO: loop at ${iteration} iterations — continuing past safety limit`, + `YOLO: loop at ${unproductiveIterations} unproductive iterations — continuing past safety limit`, ); } else { await deps.stopAuto( ctx, pi, - `Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`, + `Safety: loop exceeded ${MAX_LOOP_ITERATIONS} unproductive iterations — possible runaway`, ); finishTurn("stopped", "manual-attention", "max-iterations"); break;