On this page

On this page

Tools

Tool-loop detection

After three consecutive identical tool errors (the same tool, arguments, and error result), OpenClaw blocks that exact action and gives the model one recovery opportunity. It can change its approach, ask for clarification, or answer without the tool. An unchanged retry is blocked before execution and ends the turn with a bounded diagnostic naming the tool and error class. This narrow guard is independent of tools.loopDetection, including an explicit enabled: false.

Two additional guardrails are configured under tools.loopDetection:

  1. Loop detection (enabled) - disabled by default. Watches the rolling tool-call history for repeated patterns and unknown-tool retries. The rolling window contains agent-run calls, not operator polling through Gateway tools.invoke or HTTP /tools/invoke. Operator calls still pass through their normal authorization, tool policy, and hooks.
  2. Post-compaction guard - enabled whenever enabled is not explicitly false. Arms after every compaction-retry and aborts the run if the agent repeats the same (tool, args, result) triple within the window.

Set tools.loopDetection.enabled: false to disable these two additional guardrails.

Why this exists

  • Detect repetitive sequences that make no progress.
  • Detect high-frequency no-result loops (same tool, same inputs, repeated errors).
  • Detect specific repeated-call patterns for known polling tools.
  • Break context-overflow -> compaction -> same-loop cycles instead of letting them run indefinitely.

Configuration block

Global setting:

json5
{  tools: {    loopDetection: {      enabled: false, // master switch for the rolling-history detectors    },  },}

Per-agent override (optional, at agents.entries.*.tools.loopDetection):

json5
{  agents: {    entries: {      "safe-runner": {        tools: {          loopDetection: {            enabled: true,          },        },      },    },  },}

The per-agent setting overrides the global setting.

You can also enable the global rolling-history detectors in Settings → Agent Defaults → Tools in the Control UI. Reset the setting to its default to disable the rolling detectors while keeping the post-compaction guard; explicitly turning it off disables both.

Field behavior

Field Default Effect
enabled false Master switch for the rolling-history detectors. false also disables the post-compaction guard.

Execution titles do not distinguish otherwise identical exec calls. Code Mode also supplies private outcome identities: bookkeeping counters and continuation IDs do not count as progress. Automatically retained values use their original value identity rather than a fresh result-reference ID. Pending work is compared by its operation and arguments, while changed output, returned values, and errors remain meaningful. The displayed receipts and guest data are unchanged, including guest fields named telemetry or pendingToolCalls. This does not detect arbitrary semantically equivalent JavaScript rewrites or make background processes survive a Gateway restart.

For exec, no-progress hashing compares stable command outcomes (status, exit code, timed-out flag, output) and ignores volatile runtime metadata such as duration, PID, session ID, and working directory. For typed terminal failures, it also ignores diagnostic timestamps, explicit attempt or retry counters, elapsed durations, and labeled process IDs. Other text and numbers remain significant, so a new failure cause resets the streak.

For successful memory_search results, comparison preserves ordered hits and their content while ignoring per-call debug timings and the aggregate hit score, which changes with temporal decay. Changes to hit order, paths, snippets, line ranges, warnings, or other stable result metadata still count as progress. Scores and debug data remain in the delivered result; error results keep full comparison.

Outbound message-send results are hashed with volatile per-call ids (message id, file id, timestamp) stripped, so delivery IDs alone do not make repeated equivalent sends look like progress. When a run id is available, history is evaluated only within that run, so scheduled heartbeat cycles and fresh runs do not inherit stale loop counts from earlier runs.

Successful progress_card calls are compared using the saved Markdown and plan, not their write revision or receipt wording. Saved revisions and delivered receipts are unchanged, so a requested refresh still receives a newer saved revision even when the card content is unchanged. Errors and results without the tool’s private semantic outcome keep full outcome comparison.

Calls rejected by argument validation (for example exec without command) never run, but they are recorded as failed calls, so repeating one is detected like any other loop. They do not extend or end an exec failure streak.

Window observations from computer get_window_state are compared without fresh observation and element references. Pixels, element labels, values, bounds, and other observation data still count as changes. Model-facing results retain fresh references, and stale references remain invalid for subsequent input.

Outcome comparisons also ignore fresh external-content wrapper nonces, including wrapped errors and JSON results. Delivered security markers remain unchanged; payload text, status, timestamps, and durations still distinguish network results. This is a syntactic comparison: literal or copied text matching the complete wrapper format also ignores nonce-only changes. It does not authenticate content, change authorization, or modify delivered tool results.

  • For smaller models, set enabled: true. Flagship models rarely need rolling-history detection and can leave the master switch unset while still benefiting from the post-compaction guard.
  • To disable the rolling detectors and post-compaction guard, set tools.loopDetection.enabled: false explicitly. The identical-error guard remains active.

Repeated tool errors

Three identical consecutive failures allow the initial attempt and two retries. The third failure's result includes a recovery note, even when it is the last call in the batch. Unknown tool IDs and invalid arguments count as errors, though neither executes an implementation. Loop-policy vetoes do not count as failures.

Changed arguments, changed error results, and successful calls reset the consecutive-error streak. They do not release an action already blocked by this guard. A changed plan still passes ordinary approval and permission checks; it does not authorize replaying a mutation whose outcome is uncertain. There is no automatic tool retry. Another critical loop uses the existing terminal limit.

Already-running parallel calls settle without cancellation or replay. Unstarted calls in the triggering batch are blocked, and a matching call admitted earlier cannot start after the failure is observed. Final post-hook arguments determine whether an action is blocked. Once recovery becomes terminal, no unstarted call can begin, even if a different action was already admitted and awaiting preparation. Streamed calls from the same model response share the recovery opportunity: the model has not seen the note yet. Each action that reaches the identical-failure bound remains blocked. The error count follows the model's call order, not completion timing.

Committed user messages reset the streak, blocked actions, and shared critical-loop recovery allowance, including follow-ups and steering. Queued or cancelled input, context-only prompt updates, and continuation without new user input do not. Existing run, token, and timeout budgets still apply.

Post-compaction guard

After a compaction-retry following a context-overflow, the runner arms a short-window guard on the next few tool calls. If the agent emits the same (toolName, argsHash, resultHash) triple enough times within that window, the guard concludes compaction did not break the loop and aborts the run with a compaction_loop_persisted error.

The guard is gated by the master tools.loopDetection.enabled flag with one twist: it stays enabled when the flag is unset or true, and only turns off when the flag is explicitly false. This is intentional - the guard exists to escape compaction loops that would otherwise burn unbounded tokens, so a no-config user still gets the protection.

json5
{  tools: {    loopDetection: {      // master switch; set false to disable the guard along with the rolling detectors      enabled: true,    },  },}
  • The guard compares normalized outcome hashes, not raw result bytes. Meaningful changes keep it from aborting; fresh wrapper nonces alone do not count as progress.
  • It only arms in the immediate aftermath of a compaction-retry, not at other points in a run.

Logs and expected behavior

The always-on identical-error guard uses the bounded recovery described above. The optional rolling detectors use the warning and recovery sequence below:

  • Warnings come first. On OpenClaw-executed tool calls, a short system note is appended to the affected tool result so the model can change approach before a critical block. Warnings share the diagnostic log's rate limit, rather than appearing on every repeated call. The raw outcome is recorded before the note is added, so warning text does not count as progress.
  • Blocking follows once a pattern persists past the warning threshold.
  • Repeating wait with identical arguments and outcomes ten times blocks the next wait. Changed outcomes reset the streak. This uses the same recovery response and terminal handling described below; it does not cancel a tool call that is still executing.
  • In the embedded agent loop, the first critical loop blocks the whole tool batch before any tool in that batch runs. The model then gets one more response with its normal tools.
  • During that response, the model can answer, ask a question, or continue with a different tool or different arguments.
  • Another critical loop for the same accepted user input blocks its whole batch and ends the run. Committed new user input starts with a fresh shared recovery allowance.
  • The post-compaction guard emits compaction_loop_persisted errors naming the offending tool and identical-call count.
Was this useful?