Chapter 43: Stuck Loop Detection

"Add a detector for repeated calls" — but the worst stuck loop on record repeated a call that reported success every single time.


Quick Track (Master the core in 5 minutes)

  1. Detectors are evidence aggregation, not one rule — the public roster has nine paths
  2. Three verdicts: Continue, Nudge, ForceStop. A nudge that never escalates is decoration
  3. Errors get a wider budget than successes — retrying a failure is legitimate work
  4. Detection bounds repetition — it cannot prevent the first destructive call
  5. The fix for that lives in the tool contract, not in the detector

10-Minute Path: 43.1-43.2 -> 43.5 -> Kocoro OSS Lab


Public-source snapshot: implementation details are examples from public Kocoro origin/main at commit 4ec6772, reviewed 2026-07-27. Treat the invariant as the lesson and re-check constants against current source.

43.1 Starting with a Write That Reported Success Every Time

2026-05-13, production. An agent calls file_write. The call has a path but no content field.

Go's json.Unmarshal cannot distinguish "field missing" from "field present with zero value" on a strongly-typed struct. So content arrives as "". And os.WriteFile is perfectly happy to write an empty string — it truncates the user's existing file to zero bytes and returns no error.

The tool returns IsError: false.

The model reads that result, sees success, and — because the file it just destroyed still doesn't contain what it wanted — tries again. And again.

Now ask what the loop detector did.

It fired. The regression test that replays this pattern records that pre-fix, duplicate detection tripped on the fourth identical call. The detector was not blind and it was not broken.

It was just too late to matter. The file was already destroyed on call one.

That is the uncomfortable lesson, and it is narrower than "add a detector for this." Loop detection bounds repetition — how many times you do the wrong thing. It cannot bound the first wrong thing. A tool that reports success while truncating a file has already caused its damage before any detector has two data points to compare.

Notice also which signals were unavailable. No-progress detection? Every call reported progress. Same-tool-error detection? There were no errors. Only the duplicate path had anything to work with, and duplicates are deliberately given room — a repeated call is the normal shape of an agent working through a list.

43.2 Nine Paths, Because One Signal Is Not Enough

The public roster has nine detection paths, evaluated in order with first-match-wins. They exist as separate detectors because they catch genuinely different shapes of stuck:

EmptyThink fires on two consecutive think({}) calls — ritual empty thinking after native interleaved reasoning. ToolModeSwitch and SuccessAfterError catch redundant visual verification after something already worked. ConsecutiveDuplicate catches back-to-back identical calls; ExactDuplicate catches the same call reappearing spread across a window, which is the read→edit→read→edit→read shape. SameToolError catches one tool failing the same way repeatedly. FamilyNoProgress catches related tools circling one topic. SearchEscalation catches a trailing run of searches producing nothing usable. NoProgress is the generic backstop: same tool, too many times, regardless of arguments.

The reason this is nine and not one: a single "repeated call" rule either fires on legitimate iteration or misses the subtle cases. Searching four times with different queries isn't a loop — searching four times and citing nothing is. Reading ten files isn't a loop — reading the same file after every failed edit is.

Each detector encodes a different definition of "no progress." That's why the roster grows from incidents rather than from first principles.

43.3 Three Verdicts, and Why Errors Get More Room

A detector returns Continue, Nudge, or ForceStop.

A nudge injects a message telling the model it appears to be repeating itself, and gives it a chance to change approach. ForceStop injects a message, makes one final model call with no tools so the model can summarize where it got to, then exits the loop. That last detail matters: stopping without a summary strands the user with no idea what happened. A graceful stop is a stop that explains itself.

Now the interesting asymmetry. The constructor defaults are 3 for back-to-back duplicates and 5 for spread-out duplicates — raised from 2 and 3, because the stricter values over-fired on legitimate refactor and re-search loops — but when every call in the run is an error, the budget doubles.

Why be more permissive with failures? Because retrying a failing operation is legitimate work. A network call that fails twice and succeeds on the third attempt is a system working correctly. Whereas the same call succeeding twice with the same arguments is either wasted work or a lie. Repeated failure is often progress; repeated success rarely is.

There's a matching rule in the other direction: if the most recent call succeeded and the run had errors earlier, the duplicate detectors skip. The model has just recovered, and punishing it at the moment it got unstuck is precisely wrong.

Nudges also need their own bound. An unbounded nudge is decoration — the model gets told it's looping, ignores it, gets told again. Escalation within a rolling window turns the nudge into an actual control.

43.4 The Fix for §43.1 Is Not a Detector

Which brings us back to that zero-byte write.

You could try to build a detector for it — "flag repeated successful calls with identical arguments." But that's a heuristic patch over a broken contract, and it would fire constantly on legitimate batch work.

The real fix is upstream: make the failure legible. Every tool's Run explicitly checks that each field in its Required list is non-zero immediately after unmarshalling, and returns a typed validation error rather than a hand-rolled error result.

And the error's prefix is load-bearing. A [validation error] marker lets a dedicated check short-circuit same-tool, same-args, three-consecutive validation errors straight to ForceStop — well below the general all-errors budget, which wouldn't trip until call seven. Return a hand-rolled IsError: true without the prefix and you lose the early stop, falling back to the slower generic path.

The general principle is worth stating on its own: a detector can only see what the tool layer reports. A tool that reports success for doing nothing will still be caught eventually — but only after it has already acted. Detection is the second line of defence. The first is tools that tell the truth.

43.5 Detectors Get Removed Too

A Sleep detector used to exist — bash commands containing sleep N were treated as a stuck-loop signal, on the reasoning that a model writing sleep 5 && retry is usually waiting for external state that will never change.

It was removed in May 2026. The reason:

produced false positives on legitimate parallel-sleep tasks. Real polling is now covered by ExactDup (same command repeated), NoProgress (many bash calls without progress), and the idle watchdog (silent long-running commands).

Two things worth taking from this. First, the removal was justified by showing the failure mode was already covered by three other signals — not by deciding it didn't matter. Second, a detector whose false positives outnumber its catches is worse than nothing, because it trains you to ignore the mechanism.

This is why Chapter 32's detector table is explicitly labelled historical. It documents an earlier roster, and rosters change as incidents accumulate. Detector counts are not architectural invariants. If your system has a detector nobody can name an incident for, it's a candidate for deletion.

43.6 Snapshot Evidence

ObservationSource at 4ec6772
Nine detection paths, first match winsloopdetect.go L39
Consecutive-duplicate default of 3 (raised from 2)loopdetect.go L296
Spread-out duplicate default of 5 (raised from 3)loopdetect.go L297
All-errors budget doubles the thresholdloopdetect.go L471
Validation-error signature short-circuitloopdetect.go L779
Sleep detector removal and its coverage argumentloopdetect.go L55

These describe one dated implementation, not a universal contract.

43.7 Common Pitfalls

Treating repeated calls as uniformly suspicious. Batch work repeats calls. Recovery repeats calls. Only unproductive repetition is a loop.

Nudging without escalation. If a nudge can fire indefinitely, it's a log line, not a control.

Force-stopping without a summary. The user is left with a truncated run and no explanation. Spend the one extra tool-free call.

Detecting instead of fixing the contract. The §43.1 incident is a tool-validation bug. Any detector you build for it is a workaround that will misfire elsewhere.

Keeping detectors nobody can justify. A detector without an incident behind it is untested intuition that will eventually false-positive on real work.

Exempting tools carelessly. Some tools legitimately repeat and need exemption from duplicate detection — but a blanket exemption removes them from genuine stuck detection too. Exempt narrowly.

Kocoro OSS Lab (10 minutes)

  1. Read the Sleep removal comment. Note the structure: the failure it caught, the false positives, and the three signals that now cover it. That's what a justified deletion looks like.
  2. Find the all-errors budget line and reason about why doubling is the right direction rather than halving.
  3. Audit one of your own tools: for every field in its required list, what happens if it arrives as the zero value? If any answer is "it succeeds and does nothing," you have the §43.1 bug.

Key Points

  1. Detection is evidence aggregation. Nine paths exist because "no progress" has nine different shapes.
  2. Errors deserve a wider budget than successes. Repeated failure is often work; repeated success rarely is.
  3. A stop must explain itself. ForceStop spends one tool-free call so the user learns where the run got to.
  4. Detection bounds repetition, not the first act. The zero-byte write was caught on call four — after the file was already gone.
  5. Rosters evolve from incidents. Detectors get added when something breaks and removed when their false positives outweigh their catches.

Next: Chapter 44 covers the other half of tool-layer honesty — deciding which calls are actually safe to run at the same time.

Cite this article
Zhang, Wayland (2026). Chapter 43: Stuck Loop Detection. In AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems. https://waylandz.com/ai-agent-book-en/chapter-43-stuck-loop-detection/
@incollection{zhang2026aiagent_en_chapter-43-stuck-loop-detection,
  author = {Zhang, Wayland},
  title = {Chapter 43: Stuck Loop Detection},
  booktitle = {AI Agent Architecture: From Single Agent to Enterprise Multi-Agent Systems},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book-en/chapter-43-stuck-loop-detection/}
}