zeroclaw-labs/zeroclaw · error
Agent loop aborted: identical tool output detected {} consec
Error message
Agent loop aborted: identical tool output detected {} consecutive times What it means
A second, time-gated runaway guard: once the loop has run at least pacing.loop_detection_min_elapsed_secs, each iteration hashes the detection-relevant tool output; 3+ consecutive identical hashes abort the turn. It is fully disabled when loop_detection_min_elapsed_secs is not configured (backwards-compatible default).
Source
Thrown at crates/zeroclaw-runtime/src/agent/turn/results_collect.rs:205
*last_tool_output_hash = Some(current_hash);
}
// Bail if we see 3+ consecutive identical tool outputs (clear runaway).
if *consecutive_identical_outputs >= 3 {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
.with_category(::zeroclaw_log::EventCategory::Tool)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"model": model,
"iteration": iteration + 1,
"consecutive_identical": *consecutive_identical_outputs,
"trace_id": turn_id,
})),
"tool_loop_identical_output_abort"
);
anyhow::bail!(
"Agent loop aborted: identical tool output detected {} consecutive times",
*consecutive_identical_outputs
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::loop_detector::{LoopDetector, LoopDetectorConfig};
use crate::agent::tool_execution::ToolExecutionOutcome;
use zeroclaw_tool_call_parser::ParsedToolCall;
const RATE_LIMIT_ERR: &str = "Rate limit exceeded: too many actions in the last hour";
fn outcome(output: &str, success: bool) -> ToolExecutionOutcome {View on GitHub (pinned to 88bb9c8533)
Solutions
- Find the tool producing identical output via the logged trace_id, then change the prompt so the model varies strategy or stops
- If the polling is intentional, make outputs distinguishable (timestamps, attempt counters, state hashes) so real progress is visible
- Re-examine pacing.loop_detection_min_elapsed_secs: raise the gate or unset it for deliberate long-poll tasks (accepting the safety trade-off)
- Fix the external condition the agent is waiting on so the loop terminates
Example fix
// before: tool returns a constant string while waiting
fn status(&self) -> String { "not ready".into() }
// after: include observable state so identical rounds are distinguishable
fn status(&self) -> String { format!("not ready (checked_at={}, attempt={})", now(), self.attempts) } Defensive patterns
Strategy: validation
Validate before calling
let mut last: Option<u64> = None;
let mut streak = 0;
for round in rounds {
let h = hash_detection_relevant(&round.outputs);
streak = if last == Some(h) { streak + 1 } else { 0 };
last = Some(h);
if streak >= 3 { break; } // stop before the runtime aborts the turn
} Try / catch
match agent.run_turn(req).await {
Err(ref e) if e.to_string().starts_with("Agent loop aborted: identical tool output") => {
// the task is stuck on unchanged state: change strategy, not retry the same loop
}
other => other,
} Prevention
- Make polling tools include changing state (timestamps, counters) in output
- Define the exit condition for watch-style tasks up front
- Set pacing.loop_detection_min_elapsed_secs only when you want time-gated detection
- Prefer event-driven triggers over agent polling when possible
When it happens
Trigger: Long-lived loops whose tools return byte-identical detection-relevant outputs round after round — polling a file that never changes, idempotent no-op writes — after the minimum-elapsed gate opens.
Common situations: Watch/poll style prompts ('keep checking until X') where X never happens; tools with cached or constant responses; pacing config enabling detection on tasks that legitimately poll slowly.
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/5c253d7939d2bf97.
Report an issue: GitHub.