The frustrating part of a duplicate charge is that no line of code is wrong. An agent calls a refund tool, the tool succeeds, and the network drops the response on the way back. The harness retries, the tool has no memory of the first call, and the refund runs again. One documented case describes a customer refunded $89.99 four times from a single return because the response was lost and the retry was treated as a fresh attempt side effects and sagas.
The retry itself is correct behavior. Distributed systems are lossy, and silently dropping a request is worse than repeating one. The structural problem is that agent frameworks retry at the model layer: the model sees an error, decides to try again, and calls the tool again. Nothing in that loop tracks whether the first call already changed the world the idempotency problem. Prompt instructions like "only call the payment tool once" do not help, because the model genuinely does not know the first call succeeded.
Durable execution engines narrow the gap but do not close it by default. Restate journals each step so a restarted handler skips finished work, yet a worker killed after the external effect landed and before the result was journaled re-executed the step: a mock payment API received two charges in 3 of 3 trials, and an independent reporter described 30 of 30 trials against a receiver that could not deduplicate durable agents tested. Passing an idempotency key derived from durable state made the second request a no-op in 3 of 3 trials, as long as the receiver honored the key.
That is the case for classifying by effect rather than trusting the model. The classifier is not a safety label for a dashboard. It is the input to the routing decision that determines whether a call fires immediately, fires with a log, or waits for a person.



