AI Agents Keep Producing Duplicates? The Root Cause Is a State Modeling Error

The root cause of duplicate AI Agent dispatches is a state modeling error, not a retry logic flaw.
The article argues that duplicate AI Agent dispatches are usually misdiagnosed as missing retry guards or idempotency keys, but the real issue is state modeling: combining two independent questions into one enum field leaves the system unable to distinguish "not yet confirmed" from "truly failed." StareBrain's dual-axis model separates DeliveryState from AcceptanceState, derives combined states instead of storing them, and uses structural invariants like tryAccept() to make illegal transitions unreachable. It also distinguishes semantic mismatch from unconfirmed delivery and stresses binding dispatchId, providerObjectId, and acceptance so retries cannot borrow evidence from other runs.
AI Agents repeatedly producing duplicate dispatches in automated tasks is a common problem. Many teams' first instinct is to check whether a retry guard is missing or whether the idempotency key has failed. But StareBrain points out in its engineering blog that the real root cause of these bugs is often not a logic flaw, but a data modeling error: cramming two independent state problems into a single enum field.

How a Single-Enum State Creates Duplicate Dispatches
In a typical Agent execution flow, developers habitually use a single SUCCESS/FAILED enum to mark action state. The problem is that "whether dispatch evidence was obtained" and "whether that evidence meets the expected result" are fundamentally two different questions. Forcing them together loses the information needed to decide the next step.
StareBrain reconstructs the common failure chain: Agent dispatches an action → the dispatch times out before confirmation arrives → the system does not see a SUCCESS marker → it judges the action as failed → retry is triggered → the second dispatch produces a duplicate.
The core of the problem lies in the third step. "Confirmation not yet received" and "the action truly failed" are not the same condition, but a single enum forces the system to choose one of two options: either wait indefinitely, or retry at the risk of duplication.
Idempotency Key is a common mechanism in distributed systems for preventing the same operation from being executed multiple times: the caller includes a globally unique identifier when initiating a request, the server deduplicates on that identifier, and duplicate requests with the same key directly return the first result without re-executing side effects. Its premise is that "the service's write logic itself is idempotent," and that the generation, transmission, and storage of the identifier are all reliable. But in AI Agent scenarios, idempotency keys often fail: on the one hand, retries may be initiated by schedulers at different levels, so the generated keys are not the same; on the other hand, external providers called by the Agent (such as SMS gateways or email services) may not support idempotency key semantics, so the server cannot deduplicate based on them. Therefore, idempotency keys can only mitigate the problem of "the same logical action being submitted repeatedly," but cannot answer the state question of "whether the last dispatch actually succeeded." This is exactly where patches fail once the single-enum model breaks down.
Dual-Axis State Model: Separating Delivery from Acceptance
StareBrain's proposed solution is to have every action that produces an observable artifact track two independent states. DeliveryState answers "does the artifact exist," with values PENDING (waiting for provider query confirmation), CONFIRMED (the provider query returned an artifact), and FAILED (the query returned no result or errored). AcceptanceState answers "is the artifact correct," with values PENDING (not yet evaluated), ACCEPTED (the artifact matches intent), and REJECTED (the artifact exists but does not semantically match).
The two states advance independently, and the combined state is derived by computation rather than stored directly. This design lets the system distinguish three completely different recovery paths: "needs to keep waiting," "needs to be redispatched," and "needs the action definition to be corrected."
Derived / Computed State versus Stored State is the core design orientation here. So-called derivation means the system does not separately maintain an "overall state" field, but instead computes it on demand from two or more underlying atomic states. For example, "confirmed and accepted" corresponds to CONFIRMED + ACCEPTED, while "confirmed but rejected" corresponds to CONFIRMED + REJECTED. The benefit is eliminating redundancy: if both the combined state and the atomic states are stored, they may become inconsistent in update ordering, producing hard-to-diagnose "ghost states." The cost is that querying combined conditions requires explicit computation or materialized views. In practice, teams often persist only atomic states on the write side, and derive combined judgments through pure functions on the read side, thereby guaranteeing a Single Source of Truth.
Structural Invariants: Enforcing Safety with a State Machine, Not Conventions
Merely separating the two fields is not enough. The key is making illegal state transitions unreachable at the code level. StareBrain achieves this with a tryAccept() method: only when delivery == CONFIRMED is the acceptance state allowed to transition from PENDING. Otherwise the method throws an exception, indicating that "the provider query must be run first."
This means there is no path in the code that can mark an unconfirmed dispatch as accepted. Safety does not depend on developers remembering to follow a convention; it is enforced by the state machine.
For the redispatch decision, the logic of shouldRedispatch() is equally clear: when delivery == PENDING, never redispatch; the action remains in a DENIED_UNRESOLVED state and enters the reconciliation process. Only when delivery == FAILED (confirmation failed) or acceptance == REJECTED (semantic mismatch) is redispatch allowed.
Structural Invariant refers to a constraint enforced by the type system, constructors, or method signatures, such that it cannot be violated under any execution path. It stands in contrast to "conventions that rely on documentation and code review to remind developers to comply." Classic implementation techniques include: restricting object creation with private constructors plus static factory methods; using enums plus state transition tables to reject illegal transitions at runtime; and in more strongly typed languages, using type parameters or algebraic data types to make illegal states unrepresentable at compile time. tryAccept() belongs to the second category: it explicitly checks preconditions inside the method body and throws an exception if they are not met, making the path of "accepting before confirmation" unreachable at runtime. Compared with comments and coding standards, the advantage of structural invariants is that they do not depend on human memory and remain effective as the code evolves.
Two Failure Modes, Two Recovery Paths
The dual-axis model reveals an easily overlooked fact: "semantic mismatch" and "unconfirmed delivery" are two completely different kinds of failure and require completely different handling.
Semantic mismatch (CONFIRMED + REJECTED): the artifact does exist and has been confirmed, but its content is wrong—wrong recipient, wrong content, wrong payload. The recovery method is to correct the action definition and rerun. The original dispatch record is evidence that the event occurred and does not need to be rechecked.
Unconfirmed delivery (PENDING): the artifact has not yet been verified. At this point you should not redispatch; instead, you should run a provider query. If the artifact exists, update the delivery state to CONFIRMED and evaluate acceptance; if it does not exist, only then do you have a failure that truly needs handling.
Persistent Triplet: Binding Proof Across Retries
In real systems, state records must remain queryable across retries and process restarts. StareBrain's ActionRecord persists actionId, dispatchId, providerObjectId, the two state enums, and three timestamps together.
The significance of storing dispatchId together with providerObjectId and acceptance is that retries cannot accidentally borrow evidence from another run. Each dispatch is an independent record bound to its own artifact identifier. In the real-world Android SMS dispatch case, the "sent folder check" itself is not evidence; binding the confirmed sentMessageId to that dispatch's dispatchId is evidence. Without this link, you only know that "something was sent," not whether "what was sent was the right thing."
Reconciliation is the standard approach in distributed systems for handling "unknown state": when the result of a transaction cannot be determined immediately, the system does not guess or retry, but instead marks the entry as pending reconciliation and hands it to an independent, reentrant inspection process that periodically queries the external system's true state until a conclusion is reached. The key difference from "retry" is that retry assumes the previous operation failed and actively executes the side effect again, whereas reconciliation assumes the result is unknown and performs only read-only queries, so it does not create new side effects. In this case, a dispatch with delivery == PENDING is exactly what enters the reconciliation process rather than the retry queue. A reconciliation mechanism usually requires three supporting elements: a sufficiently long and configurable query window, an idempotent query interface, and an evidence-binding relationship that can correctly write query results back to the original dispatch record. The latter is precisely the purpose of storing dispatchId in association with providerObjectId.
Implications for Agent Developers
The value of this case goes beyond the single bug type of duplicate dispatch. It points to a more general pattern: when reliability problems arise in Agent systems, developers tend to patch at the control-flow level—adding retry guards, idempotency keys, and retry limits. But if the underlying state model itself is wrong, these patches are merely spinning at the wrong level of abstraction.
Breaking composite states into independent dimensions, replacing storage logic with derived logic, and replacing manual conventions with structural invariants—this methodology applies to any Agent system that needs to track external side effects. Retry logic does not need to "remember" when to retry; the structure of the state machine itself rules out incorrect retry paths.
One-line Summary
The root cause of duplicate AI Agent dispatches is a state modeling error, not a retry logic flaw.
Paragraph Summary
The article points out that the root cause of repeated duplicate dispatches by AI Agents is usually misdiagnosed as missing retry guards or idempotency keys, but StareBrain argues that the real problem lies in state modeling: cramming two independent questions—"whether dispatch evidence was obtained" and "whether the evidence meets expectations"—into the same enum field, causing the system to be unable to distinguish "not yet confirmed" from "truly failed," leaving it to oscillate between indefinite waiting and risky retries. The solution is to introduce a dual-axis state model that separates DeliveryState (whether the artifact exists) from AcceptanceState (whether the artifact is correct), with combined states derived by computation rather than stored directly; and through structural invariants such as tryAccept(), illegal state transitions become unreachable at the code level. The article further distinguishes two recovery paths: semantic mismatch (CONFIRMED+REJECTED, which requires correction and rerun) and unconfirmed delivery (PENDING, which should first undergo a provider query rather than retry). It also emphasizes persisting the binding relationship among dispatchId, providerObjectId, and acceptance to ensure that retries do not borrow evidence from other runs. The core takeaway is that reliability patches applied at the wrong level of abstraction are futile.
Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.