Skip to content

Build1 publisher3 min readPublished

Hashing the arguments lets a reworded retry fire the destructive call twice

Across 34 runs on three agent frameworks, a recorder proxy shows that what the dedup key names decides whether a retried publish executes once or twice, and that surviving a SIGKILL is a separate question about where state is kept.

The Engineer · Build desk

Illustration accompanying Hashing the arguments lets a reworded retry fire the destructive call twice

What happened

  • A dev.to write-up ran 34 measurements of the same task, model and recorder proxy across three agent frameworks, split into crash recovery, idempotency and audit-under-retry cells.
  • SIGKILLed while paused at the approval gate, a LangGraph run backed by a durable checkpointer was restored by a new process in 0.01 seconds with zero LLM calls.
  • Without a checkpointer the resume is a fresh run: Strands repeats its average of 5.3 LLM calls and pays the full token cost a second time.
  • Under a sha256-of-arguments dedup key, the retried call hashes differently, passes the dedup check and executes the destructive publish a second time.
  • Every duplicated publish arrived with a different tool_call ID. At the protocol layer, nothing marked the operation as having already run once.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision The two keys fail differently for on-call: the content hash passes the second publish and it executes quietly, while the position key stops it and hands over a caller-bug error to triage.
  • constraint A team relying on its framework's own trace view cannot show that a destructive call ran twice, so the recording has to sit in the call path before an audit of double execution is possible.
  • exposure With issue #8764 open, a process that dies before its first checkpoint can leave the run's acceptance unrecorded, and without its own ledger the caller cannot tell lost work from work never started.
  • cost The re-run bill lands on whoever operates the agent, and it is paid for work the killed process had already done up to the approval gate.

When an agent retries, the client does not put the HTTP request it already serialized back on the wire. The model is called again with a context that now contains the timeout error, and it emits a fresh tool call: arguments reworded, field order changed, sometimes a field that was not there the first time [5]. A key computed as sha256 over those raw arguments therefore hashes something new [6]. The dedup check records a pass and no error is raised. Only the side effect happens twice [7].

The alternative key is `{workflow}:{step}:{tool}` [3]. It names where the call sits in the workflow, so the same operation at the same step yields the same key however the model rewrites its arguments [8]. That buys a second failure mode. Same position with different arguments is rejected as a caller bug. The author records this in 2.67 of the runs and describes it as by design, so intent drift surfaces as an error [9]. The third strategy is no key at all, and across the 34 runs it prevents nothing [10].

The write-up calls the per-strategy table of average duplicate executions its core result, and the figures themselves do not appear in the article text [21]. The direction still transfers, but only to retry paths that go back to the model. sha256 is deterministic, so a retry layer that replays the arguments it already serialized produces the identical key and deduplicates correctly [2]. The earlier failure in this series, where Strands double-fired publish with the identical draft, is the one shape a content hash would have caught [19][3]; the code, traces and analysis scripts are published [20].

Crash recovery turns on a different property. LangGraph's durable checkpointer keeps graph state on disk while the process is dead, so the new process reads that state back instead of planning the work again [11]. The key strategy does not affect this. Whether you get resume or redo depends on whether the state lives outside the process [13]. Say the killed run had worked through its average before reaching the gate. Finishing that one task after one crash then costs about 10.6 model calls under Strands, against 5.3 where a checkpointer restores the paused state [1].

LangGraph issue #8764 is the case to plan around. If the process dies before the first checkpoint is persisted, recovery finds no checkpoint, and nothing on disk says the run was ever accepted. On the version tested, the resume of an empty thread succeeded without raising. The author advises keeping an external acceptance ledger and not relying on the error either way [14].

Auditability scored 100% in every cell on all four facts: rationale readable, duplicate detectable, dedup provable, retry directive visible [15]. That result belongs to the proxy, which stores every attempt as its own record, including the dedup and the caller-bug rejection [16]. The framework trace surfaces missed the double-firing entirely [17].

What to watch

  • Whether the author publishes the per-strategy duplicate-execution averages the table was meant to carry.
  • Whether LangGraph closes #8764 so a missing first checkpoint raises instead of resuming an empty thread.
  • Whether position-style keys reach framework tool-calling APIs, so the runtime supplies workflow and step identity.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories