The Agent Retried the Call: Idempotency and the Duplicate-Action Failure Mode
8 min read Fullmakt Team
- agents
- agentic-actions
- traceability
- observability
- mcp
- a2a
- governance
Most of what we’ve written about agentic AI going wrong starts with a call that shouldn’t have been authenticated, authorized, or made at all — a poisoned tool description, a spoofed Agent Card, a stale delegation. This post covers a different failure, one that needs none of that: a call that was completely legitimate the first time, and then happened again.
The failure: the agent doesn’t know if it already won
An AI agent issuing a tool call over MCP or a skill invocation over A2A is, at the network level, just an HTTP client. HTTP clients time out. Connections drop mid-response. A load balancer restarts a pod after the upstream action already committed but before the 200 came back. A human client in that position has a person attached who remembers “I already clicked submit” and hesitates before clicking again. An agent has no such memory unless something gives it one — its default behavior, baked into most agent frameworks’ retry logic, is to treat “no confirmed response” as “didn’t happen” and try again.
For a read — list_invoices, get_ticket_status — a retry is free. The
failure mode only exists for calls with a side effect, and agentic AI
increasingly exists specifically to make those calls: refund_payment,
send_email, create_deployment, close_ticket, provision_client. Retry
one of those without protection and the second attempt doesn’t fail loudly.
It succeeds — a second time. The customer gets refunded twice. The
incident-response email goes out twice, to a distribution list that now
treats your agent’s alerts as noise. The deploy job runs twice, and the
second run races the first.
Why this is an agentic-AI-gone-wrong story and not just a bug
Duplicate calls aren’t new — this is the same problem payment APIs solved decades ago with idempotency keys. What’s new is the operator. A backend engineer writing a retry loop against a payment API knows to reach for an idempotency key, because the failure mode is a familiar one from their own training. An LLM composing a retry inside an agent’s tool-calling loop doesn’t reliably reach for the same discipline unless the tool schema forces it to, because the model’s context at retry time is “the last call didn’t return a result,” not “the last call may have already succeeded remotely.” Multiply that by how many actions agentic AI is now trusted to take autonomously, without a human reviewing each one before it fires, and a retry gap that used to be a rare integration bug becomes a routine consequence of how agents are built to behave under uncertainty.
It gets worse across a chain. In an A2A delegation, Agent A calls Agent B, which times out from A’s side but not from B’s — B completed the action and was mid-response when the connection dropped. A, seeing no response, retries the same skill call against B. B has no way to know this is a retry of a call it already fulfilled unless the request itself carries that fact. Nothing about A2A’s or MCP’s authentication layer addresses this: the second call presents a perfectly valid token, aimed at a skill A is genuinely authorized to invoke, requesting something A genuinely intended to happen — once.
Why “just add retries with backoff” doesn’t fix it
Retry logic with exponential backoff is the standard answer to transient failures, and it’s necessary — but it solves the client’s problem (don’t hammer a struggling upstream) without touching the duplication problem (don’t do the thing twice). Backoff changes when the second attempt happens. It doesn’t change whether the second attempt is safe. An agent framework that adds smarter retry scheduling without also making each retried call provably a no-op if it already succeeded has made the failure rarer and harder to reproduce, not fixed it.
Where the fix actually has to live
- Idempotency keys on every action, generated once per intent, not once per HTTP attempt. The agent — or the broker composing the call on its behalf — decides “refund invoice #4471” is one intent, mints one key for it, and every retry of that same intent replays the same key. The receiving side keys its side-effect table on that value, not on request arrival order.
- The idempotency check has to sit in front of the handler, not inside it. If deduplication is left to each tool or skill’s own implementation, it’s inconsistent by construction — some handlers will remember to check, some won’t, and an agent calling either one has no way to tell which from the outside. A gate that every call passes through before it reaches a handler makes “already executed” a property of the call path, not a convention individual developers have to remember.
- The record has to distinguish “retried” from “repeated.” A duplicate key with an identical payload is a safe replay. A duplicate key with a different payload — same intent, different amount — is either a bug or something worse, and it needs to be rejected and flagged, not silently executed as a new action. This is a traceability requirement as much as a correctness one: an incident review needs to be able to see “this action fired once, was retried twice, both retries were deduplicated” as a single reconstructable timeline, not three unrelated log lines that happen to share a timestamp window.
- Read-your-writes visibility for the agent, not just the backend. Part of why agents retry destructively is that they can’t cheaply check “did my last write actually land” before trying again. A broker sitting on every call can answer that from its own record of what it already brokered, closing the loop without a second round trip to the upstream system.
The business case: why this belongs at the broker, not the tool
Fullmakt sits as the broker between every agent and every credentialed call, which puts it at the one place in the stack that sees every attempt at a given action — including the retries — regardless of which tool, which protocol, or which agent issued them:
- Every executed request is logged with the identity of the call, not just its payload, in the same tamper-evident audit trail used for every other governance gap on the platform — so a retried action shows up as a retried action in the record, not as two independent-looking events an incident reviewer has to correlate by hand after the fact.
- Policy evaluation happens on every call, including retries — a call that already executed and is being retried doesn’t get a free pass just because the first attempt cleared policy; it’s evaluated again against current state, so a retry can’t succeed where a fresh call would now be denied.
- Because Fullmakt brokers the credential for every call in a chain, a retried A2A skill invocation or MCP tool call carries the same broker-issued context whether it’s attempt one or attempt three — the record ties every attempt of one intent together instead of treating each HTTP round trip as an unrelated event.
- Workspace-scoped policy can require idempotency for the action classes that need it — payments, provisioning, deployments — without every individual tool author having to remember to implement deduplication themselves, the same way credential scoping removes the need for every tool to reinvent least privilege on its own.
The commercial logic is the one that runs through every post in this series: the fix for a class of agentic failure is more durable when it lives at the one chokepoint every call already passes through, instead of being re-implemented — or forgotten — inside each individual tool, skill, or agent that happens to make the call.
FAQ
Isn’t this just a payments-API problem? It started there, but agentic AI generalizes it to any action with a side effect — sending a message, provisioning a credential, triggering a deploy, closing a ticket. Any call an agent might retry under network uncertainty is a candidate, and agents retry far more calls, more autonomously, than a human operator typically would.
Doesn’t the LLM just know not to repeat an action? Not reliably. An agent’s retry logic usually operates below the model’s own reasoning — a tool-calling framework resending a timed-out request doesn’t necessarily involve the model deciding “should I do this again,” so prompting the model to be careful doesn’t reach the code path that actually causes the duplication.
Why can’t the receiving tool just handle idempotency itself? Some can and should, but relying on every tool author to implement it correctly and consistently is the same distributed-discipline problem as relying on every tool to enforce least privilege on its own — it works until the one tool that didn’t becomes the incident.
How is this different from the “lethal trifecta” data-exfiltration pattern? That failure is about data leaving through a path it shouldn’t. This one is about an action happening more times than it should — different mechanism, same root shape: an agent operating with more autonomy than the surrounding system was built to safely absorb.
What should an incident review look for? Whether the audit trail can show a single action’s full attempt history — first call, timeout, retry, dedup outcome — as one connected record. If retries only show up as separate unrelated log lines, the traceability gap is often what let the duplicate action go unnoticed as long as it did.
A login handshake proves an agent is who it says it is. It has nothing to say about whether the call it’s making right now is the first attempt at an action or the second — that has to be answered by the system the call passes through, on every attempt, or a network hiccup quietly becomes a duplicate refund nobody notices until a customer does.