Skip to content

feat(session): a loop guard that can see a loop - #105

Merged
filipeforattini merged 1 commit into
mainfrom
feat/loop-guard
Sep 4, 2026
Merged

feat(session): a loop guard that can see a loop#105
filipeforattini merged 1 commit into
mainfrom
feat/loop-guard

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Sep 4, 2026

Copy link
Copy Markdown

What was there

DOOM_LOOP_THRESHOLD = 3 compared the last three parts of the current assistant message and
required byte-identical serialized input. That meant:

  • one interleaved text or reasoning part reset it permanently — reasoning models emit them constantly;
  • it was scoped to one message, and every step of a turn is its own message, so a loop across steps
    was invisible;
  • it ignored whether the calls succeeded or failed;
  • when it fired it called permission.ask("doom_loop"), whose wait has no bound. Our only defence
    against a loop was itself a way to hang. No test tripped it.

What replaces it

session/loop-guard.ts — pure, tested — judges repetition across the whole turn:

  • only settled tool calls count, so text and reasoning between calls no longer break the chain;
  • a call counts as a repeat when the tool, the arguments and the result are the same. Identical
    calls returning different results is what polling looks like, and is left alone;
  • a repeated failure counts the same as a repeated success;
  • a call the guard already refused stays part of the streak, so the correction cannot reset the very
    streak it started.

The guard is consulted in tools.ts before the call runs, so a call whose answer is already
known is never made again. The correction arrives as that tool's own result, quoting the arguments
and the answer being ignored, and naming the ways out. If the model keeps going anyway, the turn
ends (ctx.blocked) rather than anyone being asked whether to continue.

experimental.loop_guard sets the thresholds (3 and 5 by default) or false disables; a
doom_loop: "allow" permission rule still means "let it repeat", so that knob is not left dead.

Verification

  • Unit tests seen failing against the old detection shape (interleaved parts, polling) and passing
    after.
  • Integration test seen failing first — with the guard disabled the same call ran three times
    untouched — then passing: the second identical call is answered by the correction, the third ends
    the turn.
  • bun typecheck across the workspace; packages/redcode session suite 435 pass.
  • Two failures reproduced on unmodified sources on this machine and unrelated: the feat(session): end a turn that has stopped producing anything #103 watchdog
    test and finalizes assistant when cancelled before processor creation completes, both timing
    out under local load. CI arbitrates.

https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The previous detector could only see three byte-identical parts in a row
inside one assistant message, so any reasoning part hid the loop and a loop
across steps was invisible. When it did fire it asked the user a question with
no bound, which made our only loop defence another way to hang.

Judge repetition across the turn, treat a changed result as evidence the world
moved, and answer the repeated call with a correction the model can act on.
Stop the turn only if that correction changed nothing.

Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
@filipeforattini
filipeforattini merged commit 8c43207 into main Sep 4, 2026
7 checks passed
@filipeforattini
filipeforattini deleted the feat/loop-guard branch September 4, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant