Build1 publisher2 min readPublished
Retrying a timed-out tool call can make an AI agent refund the same order twice
Agent loops can repeat a state-changing tool call in eight separate ways, a dev.to post argues, and each can duplicate a refund whose reply was lost. The post's fix is a stable operation ID tied to the payload, with UNKNOWN outcomes held until someone checks the provider.
The Engineer · Build desk

What happened
- A dev.to post walks through a refund where the provider commits the payment, the reply is lost, the agent sees an error, and the framework retries the tool.
- After the lost acknowledgement, both 'no refund yet' and 'refund already done' fit what the agent saw, a state the post calls an ambiguous outcome.
- The post lists eight separate paths to a repeat, including model retries, checkpoint resumes, supervisor redispatch, MCP re-attempts and a user asking to try again.
- Its central rule is that an UNKNOWN outcome does not give the agent permission to execute the action again.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Any of the eight repeat paths can cross the side-effect boundary, and no single retry layer sees the others, so deduplication has to sit at the tool boundary and key on the action.
- exposure A dedup store keyed on operation ID alone would treat the invoice to bob as a replay of the one to alice and drop a real action.
- cost Honouring UNKNOWN means some refunds, deployments and paid provisioning calls stop until a person or trusted system resolves them, and someone has to own that queue.
"A timeout describes what the caller observed. It does not prove what the external provider did," the post says [2]. Its author does not treat this as a model problem or as AI hallucination. The post describes the underlying problem as distributed-systems uncertainty at the boundary between intent and external effect [11]. The worked example is a refund. The same failure covers a second email, booking, order, paid resource or deployment [10].
Agents do not change the underlying problem. They add ways to repeat. The post counts eight [1] and says each one can be reasonable on its own [12]. The danger comes when one of them crosses a side-effecting boundary without carrying the identity and outcome of the logical action [12]. I think that rules out guarding at any single layer. A framework's retry counter has no view of a supervisor redispatch or a checkpoint resume [4]. A user typing "try that again" is a retry path too, and nobody attached a counter to it [4].
The post's fix separates two identities. Transport identity answers which attempt this is. Logical identity answers which real-world action it is [6]. Request IDs, tool-call IDs, trace IDs, retry counters and timestamps usually identify attempts [5]. A key like refund / order_123 should stay the same across every retry of that one refund, even as each attempt gets a new request ID [5].
The second half of the design is the best part of the post. The operation identity has to be bound to the fields that change the effect: amount, destination, message body, recipient, resource configuration [7]. The example reuses send_invoice_4821 with [email protected] on attempt 1 and [email protected] on attempt 2 [7]. That pair must produce a conflict or a new intentional operation. Otherwise the deduplication layer collapses two distinct actions into one [7].
The execution model has at least three outcome states. The rule the post calls critical is "UNKNOWN is not permission to execute again." [8] "This sounds conservative because it is," it adds [13]. The cost falls on liveness. When provider truth is unavailable, a high-impact operation may stay blocked until a human or a trusted system resolves it [9]. The post accepts that cost where a duplicate would be expensive or irreversible [9].
The post leaves the resolution step to that human or trusted system [9]. The logical identity is what would let a machine do the check. The question for the payment provider is whether refund / order_123 exists. Attempt 2's request ID cannot ask that question [5][6].
What to watch
- Whether agent frameworks and MCP servers carry one logical operation ID through retries, checkpoint resumes and redispatch, or mint a fresh tool-call ID per attempt.
- Whether payment, booking and provisioning APIs used as agent tools let a caller look up an effect by logical operation ID; without that, UNKNOWN resolves only through a human.