A Timeout Is Not a Failed Action: Review This Refund Retry
A refund assistant receives approval to return $80. Its first request times out. The author adds a retry, gets a successful response and asks you to approve the change.
The release note says, “Failed requests now get another chance.” There is a problem with that sentence: the client observed a timeout. It has not established that the refund failed.
Here is the review packet. All IDs, amounts and events in this article are synthetic. They come from AFL’s offline When the Agent Acts Twice workbook, not a customer incident or a payment-provider integration.
Approved operation: R-101
Tenant: T-1
Order: ORD-7
Amount: 8000 cents USD
request: key afl-refund:R-101:attempt-1
timeout: no usable response
request: key afl-refund:R-101:attempt-2
response: RF-0002, succeededBefore reading further, write down what this proves. Then write down what it leaves open.
Two explanations still fit
The first request may have failed before committing an effect. In that case, the second request could create the only refund.
Or the first request may have committed, with its response lost on the way back. A retry under a different key could then create another refund.
The client trace is consistent with both. Even the second response’s refund ID does not establish what happened to the first request without knowing the provider’s ID allocation and storage behavior. Asking for more confident prose from the agent will not close that gap.
The next useful artifact is the provider’s record for this logical operation. In a real integration, that might require a supported lookup, reconciliation report or operator investigation. The lab exposes an authoritative effect ledger so the distinction is visible.
Run the failure
Download and extract the workbook using the link at the end. From the extracted issue directory, with Node 22 or newer, run:
node lab/run.mjs faultyThe lab needs no API key, package installation or network access. It writes synthetic records to a new temporary directory and prints that location for inspection.
Compare four fields in the JSON output:
| Field | Question it answers |
|---|---|
clientTrace |
What requests and responses did the application observe? |
result |
What did the application tell its caller? |
authoritativeEffects |
What committed in the simulated provider? |
effectCount |
How many effects did this run produce? |
In this deliberately faulty run, the provider commits the first effect before throwing the timeout. The retry uses a new key, so it commits a second effect. The client still reports success.
That is the demonstrated failure. It would have been premature to claim two refunds from the initial client trace alone.
State the invariant before editing code
A useful regression statement is:
Given one approved operation, when the provider commits its effect but the response is lost, retrying must not create a second effect for that operation.
This is stronger than “the retry helper was called” or “the response says succeeded.” It also permits multiple requests: under the lab’s provider contract, repeated requests with the same key and payload return the original result.
Run the regression against the faulty implementation:
LAB_IMPLEMENTATION=faulty node --test test/regression.test.mjsThe intended failure is an assertion with actual 2, expected 1, and exit status 1. A missing Node installation or syntax error is not evidence that the regression caught the defect.
Then run the repair:
node lab/run.mjs repaired
node --test test/regression.test.mjsThe repaired client stores the approved operation before execution and uses its stable identity across attempts. The simulated provider records the key and effect atomically. There are repeated requests, but one committed effect.
Approval belongs to the operation
An order ID alone is too broad: one order may legitimately have two separately approved refunds. An attempt ID is too narrow: every retry becomes a new operation. The lab uses a distinct approved operation ID bound to the tenant, order, amount and currency.
Changing those fields is a new authorization question. The client must not carry an old approval onto a changed amount merely because the model describes it as “the same refund.”
This connects to reviewing agent permissions. IAM determines which APIs an agent can reach. The execution system must still constrain what a permitted call means, who approved it and whether it has already happened.
What the repair does not prove
The lab invents a deliberately clear provider contract: keys never expire, matching repeats return the same final result, and the provider serializes its check-and-commit step. Its local JSON files demonstrate recovery with intact files, not distributed locking or power-loss durability.
A production provider may retain keys for less time than your queue can redeliver work. It may return an asynchronous status. A second process may race to execute the operation. Each difference needs its own design and evidence.
Use the workbook’s separate answer key after making your release decision. The point of the exercise is to practice asking for evidence that can distinguish explanations, then writing a check that fails for the consequence you care about.