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

  1. Find the tool producing identical output via the logged trace_id, then change the prompt so the model varies strategy or stops
  2. If the polling is intentional, make outputs distinguishable (timestamps, attempt counters, state hashes) so real progress is visible
  3. Re-examine pacing.loop_detection_min_elapsed_secs: raise the gate or unset it for deliberate long-poll tasks (accepting the safety trade-off)
  4. 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

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.