Skip to content

Build1 publisher3 min readPublished

Your agent's retry logic is reading a timeout as a fact it does not have

A dev.to post argues that recording a timed-out tool call as failed is what authorizes duplicate payments and tickets, and proposes a third state that blocks retry and routes to reconciliation.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • The post asserts that if a system records an AI agent's timed-out HTTP or browser tool call as 'failed', the agent has a blind spot.
  • A network timeout does not mean the operation failed on the remote server; it means the connection closed before the client received the answer.
  • If the server processed the mutation, retrying the call blindly will create a duplicate artifact: a double payment, a duplicate ticket, a repeat email, or a redundant article.
  • If the system records the timed-out call as 'succeeded', it is hallucinating certainty.
  • The missing state is outcome_unknown, a first-class operational status that halts automatic retries, records the unconfirmed mutation, and hands off execution to an explicit reconciliation loop.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A post on dev.to argues that agent frameworks carry a specific and fixable bug: when an HTTP or browser tool call times out, they record the action as `failed`, which is an assertion about the remote server that the client is in no position to make [1][2]. That matters because `failed` is the state most retry logic reads as permission to go again, and if the server did commit the mutation, the retry produces a second artifact: a double payment, a duplicate ticket, a repeat email, a redundant article [3].

Writing `succeeded` instead is, in the author's words, hallucinating certainty [4]. The proposal is a third status, `outcome_unknown`, treated as first-class: it halts automatic retry, records the unconfirmed mutation, and hands execution to an explicit reconciliation loop [5]. The author presents it as the concrete state machine behind an earlier argument that agents need action receipts rather than purely semantic memory [23].

The useful part of the piece is where it draws the boundary. Errors before the first byte leaves the socket, such as DNS failure, missing local credentials or a refused connection, leave the world unchanged, so the action is deterministically unexecuted and safe to retry [7]. Once bytes are on the wire, transport failure stops reporting server state at all: the server may have committed and then crashed while serializing its response, or a proxy may have quit after 30 seconds while the backend worker ran to completion [8]. The state machine routes the two zones differently, sending local socket and auth errors to REJECTED / SAFE_TO_RETRY and connection timeouts, drops and 504s to OUTCOME_UNKNOWN [6]. Collapsing the second zone into failure is, on the author's account, the root cause of automated duplicate storms [9].

Reconciliation needs evidence, and payments got there first with at-least-once delivery plus a server-side deduplication key [10]. Where a platform supports that natively, as with Stripe's `Idempotency-Key` header or GitHub GraphQL mutation keys, recovery is simply re-sending with the identical key [11]. The post's honest concession is that most web APIs, CRUD services and browser-driven surfaces offer no such key, so the caller carries the burden [12]. Its substitute is a normalized intent fingerprint computed before sending, a canonical hash over mutation type, resource target and normalized body fields [13], implemented as a sha256 over JSON serialized with sorted keys and no whitespace [14], followed by a read-after-write query to the read or search endpoint for resources the agent's own account created inside a bounded timestamp window matching that fingerprint [15].

One design note is worth taking regardless of the rest: do not overwrite state in place, because doing so destroys the record that an action sat indeterminate for hours, which is exactly what post-mortems and race-condition audits need [16]. Receipts should append transitions instead [17]. The sample receipt runs planned at 19:00:00Z, submitted at 19:00:01Z, `outcome_unknown` at 19:00:31Z carrying "Gateway Timeout 504", reconciling at 19:05:00Z, and succeeded at 19:05:02Z with the evidence "Readback from /api/articles matched title fingerprint" and external id 4407310 [18]. That is 30 seconds to ambiguity [19], four minutes and 29 seconds sitting in it [20], and five minutes two seconds end to end [21]. An in-place update would have left a receipt saying the call just worked.

The argument is mechanistic; the post carries no measurement of how often timeout-driven duplicates actually occur [22]. Two things to check in your own tool wrapper: whether it distinguishes a refused connection from a 504 today, and whether anything downstream will run reconciliation once timeouts stop being retryable. The fingerprint-and-readback path assumes a listing surface scoped to the agent's account and filterable by time [15]; where that does not exist, `outcome_unknown` is honest but terminal, and the reconciliation loop is a human being.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories