fix(auto): count only unproductive runaway iterations

This commit is contained in:
Mikael Hugo 2026-05-15 06:55:05 +02:00
parent 5faa789f52
commit d1ca3d035c

View file

@ -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;