Skip to content

Why your agent repeats an action it already took

Six causes of a repeated refund or email, from a missing stop condition to a record the agent lost, what fixes each, and the one underneath most of them. Our November rerun had no refund applied twice, with a key on the tool in both runs.

Neil Luo ·

Picture a support agent that refunds a customer's order. Forty minutes and a lot of tokens later, it refunds the same order again. Nothing crashed and nothing timed out. The agent didn't know it had already done it.

Repeated actions have more than one cause, and the fix for each is different. Most of them are yours to fix whatever you run the agent on. This post goes through all of them, then the one underneath most of them, then the small part an engine can do.

Every cause, and what fixes it

1. Nothing tells the loop to stop. An agent loop runs think, act, observe, and goes round again. If the only way out is the model deciding it is finished, an ambiguous goal leaves it assuming one more step is needed. A practitioner write-up, this one on loops that never end, calls the stop condition implicit and recommends a hard step cap that ends the loop whatever the model decides. It is a blog post, not a study, but the fix costs almost nothing. Add an explicit completion check as well. The guide on an agent that calls the same tool twice covers it.

2. The tool's answer was unclear. A partial result, an odd schema or a silent error leaves the model unable to tell whether the call worked. So it calls again. Return a result the model can read as done or failed, in plain words, and say why when it failed.

3. A retry after a timeout. The first call succeeded and its answer never came back, so the caller sent it again. This one is not about the model at all. The guide on a tool that runs twice after a resume covers it, and Stripe's page on idempotent requests describes the standard fix: a key the server uses to recognize a retry.

4. The same request arrives twice. A webhook can deliver the same event twice: Stripe's webhook documentation says an endpoint might occasionally receive the same event more than once. The agent does the work twice because it was asked twice. Deduplicate at the door, before the request reaches the agent. The guide on stopping an agent from running twice covers the case where two workers pick up one run.

5. The record was thrown away. When old messages and tool results are dropped or summarized, the evidence that an action ran can go with them. Tian Pan's post on compaction traps describes the pattern: the summary keeps the goal and loses the completion records, so the agent re-reads files it already processed and re-applies changes it already made. The same post lists double-written records and duplicate notifications among the results.

6. The record is there and the model doesn't use it. In a long context the model may not use the record even when it is there. Lost in the Middle found that accuracy drops when the relevant information sits in the middle of a long input. Chroma's context rot report tested 18 models and found that performance degraded as input length increased across its experiments.

You can tell the last two apart by checking what the model was shown at the moment it repeated. If the evidence that the action ran was in its prompt and it did it anyway, you have a reasoning problem, and the fix is a check outside the model. If the evidence never reached the model, no change to the prompt wording will fix it.

What stops it, whatever you run on

Put the check in the tool. Give each call that changes something a business key, such as refund:O1039, record the key when the call is claimed, and refuse a second call with the same key. Return a refusal the model can read ("refund already issued for this order") instead of throwing an error, so it can tell the customer instead of retrying.

One gap remains. If the process dies between the claim and the refund, the key says done when it may not be. Record that case as outcome unknown and check it against the payment provider, because the tool can't know. The guide on making agent tools safe to retry explains idempotency keys, with code, including how to pass one to a provider that accepts it.

A guard like this is easy to write yourself, and you should. It stops a repeat from being applied. It does nothing about the model trying again and again, which still costs tokens and can still reach a side effect you forgot to guard.

The cause underneath

Causes 1 to 4 are about the loop and the plumbing. Causes 5 and 6 share a root with a lot of other long-run failures: the only record of what the agent did lives in the prompt, and the prompt is something a model reads, rewrites and loses. The model is asked to remember what it did and to decide what to do next from that memory. In a plain loop, nothing outside the model checks the record before an action runs.

A longer window may delay the failure, and a better summary prompt may shrink it. Neither removes it while the record stays in the model's hands. The record has to be kept somewhere the model can't edit, and the guard has to read from there.

What an engine can fix

That is the part we build. On the hosted engine a tool can declare once_per, a template such as refund:{order_id}, and the engine refuses a second call whose filled-in key matches. Separately, the engine can list in the prompt what has already been done. It does not decide when your agent is done or fix an unclear tool answer. A retry you send is still a retry unless you send an Idempotency-Key, which the hosted engine uses to return the first answer to a repeated wake.

What we measured

In October, a 12-hour shop run on an older build, with no key on the refund tool, paid 7 orders more than once. The known issues page lists each order.

We reran the shift in November, twice, on Claude Haiku 5.5 (engine release 0.51.0, hosted). The refund tool declared once_per: "refund:{order_id}" in both runs. One run had the engine keeping state and one had the older setup.

  • Refunds applied twice: 0 in both runs.
  • Refunds that reached the tool: 17 with the state on, 16 with it off, each for a different order, and none for an order that was already paid.

The key was on in both runs, so this test can't separate what the key did from what the engine-kept state did. A call the engine refuses is never handed back to the harness, so it doesn't appear in this log either. The refund log and the graded rows are downloadable.

What we haven't shown

  • One run per setup, on a scenario we generated, with one model.
  • One guarded tool. The mechanism takes a key on any tool, but we have measured it on refunds only.
  • An action running exactly once. We don't claim that. A key on a business identifier only helps when the thing being repeated has one, and many agent actions don't.
  • Anything about another SDK. We haven't published a run of an agent built on another SDK through this scenario.

Sources and method: 11 hours 59 minutes on shift: what the agent remembered.

More posts.

  1. Does a growing transcript hurt a long agent run? Our early test, and the part we retracted

    An early September test of rebuilding the prompt at each task boundary against a growing transcript. The study reports it found more in all three cells, by less than we first claimed, and the claim that a growing context makes an agent worse did not survive our own re-test.

  2. Why an agent juggling many tasks loses track of what's done, pending and blocked

    Six reasons one agent working through many tasks reopens finished ones, forgets blocked ones and reports done too early, what fixes each, and the one underneath. We have no orchestration benchmark yet, and the post says so.

  3. Why an agent's own memory skips things

    Six reasons notes an agent writes for itself leave out the one detail that mattered, what to do about each, and the one underneath. On our shop shift, 22 of 28 stock answers were right with the engine keeping the counts and 4 of 28 with the model writing them. One run each.

  4. Why your agent forgets things after compaction

    Six reasons an agent loses details when its context is summarized and restarted, what you can do about each, and the one underneath. Includes our one-run shop-shift numbers and what they don't show.

  5. Why long-running agents lose track of counts

    Six reasons an agent's running totals drift, what to do about each, and the one underneath. On our own 900-event shop shift, 22 of 28 stock questions were right with the engine keeping the counts and 4 of 28 without. One run each, and six answers were still wrong.

  6. Where Argentic fits next to the agent SDKs and the tools around them

    A table-by-table read of the Claude, OpenAI and Amazon agent SDKs, then Mem0, Zep, LangGraph and Letta, from their own docs, with where each is ahead of us.

  7. A plain transcript loop or an engine like ours: when each is the right call

    What the plain loop does well, where it runs out, and what the engine costs you.

  8. Why your agent forgets things from a long history

    On Gemma 4 31B, with histories of about 1.2 million tokens, a loop that keeps only about the newest 100,000 tokens got 5 of 60 right and the engine got 38. What we ran and what it doesn't show.

  9. Does summarizing old context lose details? We measured it

    On Gemma 4 31B, with histories of about 1.2 million tokens, our own note-rewriting loop got 17 of 60 and the engine got 38 of 60. Most of the loop's misses were answers saying the detail wasn't in its notes (29 of 43).

  10. Resuming an agent after a crash without repeating a finished action

    We killed a real server mid-run with SIGKILL and finished the work from another process. What was not repeated, and the one case that is.

  11. Checking what your agent was shown on each turn

    Replay re-checks every prompt against what was recorded: 1,754 of 1,754 turns matched. It checks the prompt side only.

  12. Run your first agent

    Sign in to the Argentic dashboard, make an API key and run an agent that remembers. What you get, what we've measured and what isn't there yet.

  13. Undoing what a bad source caused, from the record alone

    When a source was flagged after the run, the engine undid what it could have caused, using undo functions the host supplies, and finished without it, in 156 of 156 runs: 120 scripted, and 36 live Haiku 5.5 runs, 24 of them with the compromise forced. The live runs were rerun after an engine fix; the first 12 had 2 worlds missing benign effects.

  14. Why we rebuild an agent's context instead of appending to it

    What rebuilding context from a record changes, in plain terms, and the limits of what we have measured so far.