{"record":{"id":"5c253d7939d2bf97","repo":"zeroclaw-labs/zeroclaw","slug":"agent-loop-aborted-identical-tool-output-detected","errorCode":null,"errorMessage":"Agent loop aborted: identical tool output detected {} consecutive times","messagePattern":"Agent loop aborted: identical tool output detected (.+?) consecutive times","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/agent/turn/results_collect.rs","lineNumber":205,"sourceCode":"            *last_tool_output_hash = Some(current_hash);\n        }\n\n        // Bail if we see 3+ consecutive identical tool outputs (clear runaway).\n        if *consecutive_identical_outputs >= 3 {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                    .with_category(::zeroclaw_log::EventCategory::Tool)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\n                        \"model\": model,\n                        \"iteration\": iteration + 1,\n                        \"consecutive_identical\": *consecutive_identical_outputs,\n                        \"trace_id\": turn_id,\n                    })),\n                \"tool_loop_identical_output_abort\"\n            );\n            anyhow::bail!(\n                \"Agent loop aborted: identical tool output detected {} consecutive times\",\n                *consecutive_identical_outputs\n            );\n        }\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::agent::loop_detector::{LoopDetector, LoopDetectorConfig};\n    use crate::agent::tool_execution::ToolExecutionOutcome;\n    use zeroclaw_tool_call_parser::ParsedToolCall;\n\n    const RATE_LIMIT_ERR: &str = \"Rate limit exceeded: too many actions in the last hour\";\n\n    fn outcome(output: &str, success: bool) -> ToolExecutionOutcome {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/turn/results_collect.rs#L187-L223","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: tool returns a constant string while waiting\nfn status(&self) -> String { \"not ready\".into() }\n\n// after: include observable state so identical rounds are distinguishable\nfn status(&self) -> String { format!(\"not ready (checked_at={}, attempt={})\", now(), self.attempts) }","handlingStrategy":"validation","validationCode":"let mut last: Option<u64> = None;\nlet mut streak = 0;\nfor round in rounds {\n    let h = hash_detection_relevant(&round.outputs);\n    streak = if last == Some(h) { streak + 1 } else { 0 };\n    last = Some(h);\n    if streak >= 3 { break; } // stop before the runtime aborts the turn\n}","typeGuard":null,"tryCatchPattern":"match agent.run_turn(req).await {\n    Err(ref e) if e.to_string().starts_with(\"Agent loop aborted: identical tool output\") => {\n        // the task is stuck on unchanged state: change strategy, not retry the same loop\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["loop-detection","identical-output","runaway-loop","pacing-config"],"backgroundTag":"infinite-loop-detected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}