{"record":{"id":"3d43c6b3d2f605ef","repo":"zeroclaw-labs/zeroclaw","slug":"agent-loop-aborted-repeated-prompt-required-tool","errorCode":null,"errorMessage":"Agent loop aborted: repeated prompt-required tool call '{tool_name}' with identical arguments before approval.","messagePattern":"Agent loop aborted: repeated prompt-required tool call '(.+?)' with identical arguments before approval\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/agent/turn/call_prep.rs","lineNumber":217,"sourceCode":"                        .with_attrs(::serde_json::json!({\n                            \"model\": ctx.model,\n                            \"iteration\": iteration + 1,\n                            \"tool\": tool_name.clone(),\n                            \"arguments\": scrub_credentials(&tool_args.to_string()),\n                            \"result\": repeated,\n                            \"trace_id\": ctx.turn_id,\n                        })),\n                    \"tool_call_result\"\n                );\n                if let Some(tx) = ctx.on_delta {\n                    let _ = tx\n                        .send(StreamDelta::Status(format!(\n                            \"\\u{274c} {}: {}\\n\",\n                            tool_name, repeated\n                        )))\n                        .await;\n                }\n                anyhow::bail!(\"{repeated}\");\n            }\n        }\n\n        // ── Approval hook ────────────────────────────────\n        let approved = match gate_tool_approval(ctx, &tool_name, &tool_args, iteration).await {\n            ApprovalGateOutcome::Proceed { approved } => approved,\n            ApprovalGateOutcome::Deny(outcome) | ApprovalGateOutcome::Replace(outcome) => {\n                // Streaming consumers see the denied/replaced call and its\n                // synthesized result (e.g. a DenyWithEdit replacement) as a\n                // ToolCall/ToolResult pair, as the direct path always did.\n                if let Some(tx) = ctx.event_tx {\n                    emit_tool_call_pair(tx, call, &outcome).await;\n                }\n                ordered_results[idx] =\n                    Some((tool_name.clone(), call.tool_call_id.clone(), outcome));\n                continue;\n            }\n        };","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/turn/call_prep.rs#L199-L235","documentation":"In tool-call preparation, ZeroClaw tracks signatures (tool name + arguments) of prompt-approval-required shell calls across the whole loop. When the model emits a shell call whose signature was already recorded in a previous round and still has not been approved, prepare_tool_calls aborts the turn — an explicit infinite-approval-loop guard rather than letting the model re-request the same command forever. A status delta and a WARN log with the tool and scrubbed arguments precede the bail.","triggerScenarios":"The model re-issues the exact same shell command after the earlier identical call was denied, replaced, or recorded as a duplicate; a non-interactive channel (cron, gateway) where the approval prompt can never be answered; the model retrying a failing command with identical arguments instead of changing them.","commonSituations":"Risk profile flags shell for approval in an environment with no human to approve; agent stuck in a retry loop on a command that keeps failing; approval hooks that deny without telling the model what to change.","solutions":["Resolve the pending approval: run interactively and approve/deny, or adjust the approval policy so this specific command class proceeds without a prompt.","Change the system prompt so the model varies arguments or asks the user instead of repeating an identical call.","Inspect the WARN 'tool_call_result' log for the exact tool and scrubbed arguments, then fix whatever output keeps making the model retry identically (e.g. a failing command's error).","If the environment is legitimately non-interactive, scope approval requirements down for the commands that run there."],"exampleFix":"# before — non-interactive run, model repeats `shell: cargo test` awaiting approval that never comes\n# after — pre-approve deterministic commands or instruct the model:\n#   \"`cargo test` is pre-approved. Run it once; if it fails, change the command before rerunning.\"\n# and/or relax the approval policy for that command class in the agent's risk profile","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"Catch the abort at the turn boundary, read the WARN 'tool_call_result' log for tool name + scrubbed arguments, then either resume the turn after the approval is granted/policy adjusted, or surface the tool call to the user for an explicit decision. Do not auto-retry the identical turn — it will abort again.","preventionTips":["Keep approval prompts answerable: run approval-requiring agents interactively or provide an approval hook","Pre-approve deterministic read-only commands in the risk profile for non-interactive channels","Prompt the model to change arguments (not repeat them) after a failed command","Monitor for duplicate tool-call signatures as an early loop signal"],"tags":["agent-loop","approval","shell","tools","infinite-loop-guard"],"backgroundTag":"llm-tool-loop-detected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}