DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Event-Driven AI Systems With Kafka and Autonomous Agents
  • Prompt Caching: Overriding Tokenization for Faster and More Cost-Effective AI
  • Handling Large API Responses Without Freezing the Client: A Practical Architecture With Temporal, Kafka, and RAG
  • How to Detect AI-Generated Images in C# Using an API

Trending

  • Cutting Telemetry Volume Is Not the Same as Cutting Noise
  • Bringing Graph Analytics to Snowflake With Neo4j
  • How to Design a Multi-Agent AI Framework in Python for Enterprise LLM Workflows
  • Zmanim-WP: Getting Started
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. When an iOS Retry Executes an Agent Twice: Building Effectively-Once Tool Workflows With LangGraph, MCP Tasks, Kafka, and App Attest

When an iOS Retry Executes an Agent Twice: Building Effectively-Once Tool Workflows With LangGraph, MCP Tasks, Kafka, and App Attest

Stable operation IDs prevent iOS retries from duplicating agent tools across LangGraph, MCP Tasks, Kafka, and App Attest.

By 
Uthej Mopathi user avatar
Uthej Mopathi
DZone Core CORE ·
Sep. 21, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
105 Views

Join the DZone community and get the full member experience.

Join For Free

A mobile request can fail without the server-side work failing. An iOS app may time out, lose the response after a POST has reached the service, or retry after connectivity changes while the original execution is still progressing. Apple explicitly distinguishes safe retry behavior by HTTP method and notes that URLSession can retry requests in some connection-loss cases, waitsForConnectivity can also cause the system to continue a request when connectivity returns. 

The dangerous state is therefore not “request failed,” but “completion is unknown.” If that request starts an agent that charges an account, reserves inventory, sends a message, or invokes an MCP tool, a second submission can become a second side effect.

The Retry Boundary Is the Real Transaction Boundary

“Exactly once” is too strong for a workflow crossing an iPhone, HTTP, an agent runtime, an MCP server, Kafka, a database, and an external API. Kafka can provide exactly-once guarantees within defined Kafka processing boundaries, but those guarantees do not atomically include arbitrary remote tool effects. The practical target is effectively-once behavior, and retries are expected, but every effect is guarded by a stable operation identity and converges on one committed outcome. Kafka’s idempotent producer suppresses duplicate records caused by producer retries, while transactional producers can atomically publish across Kafka partitions; the producer documentation also limits idempotence guarantees to a producer session and requires read_committed consumers for end-to-end transactional visibility. 

The operation identity must exist before the first network attempt. An iOS client can create an operationId when an action becomes durable local intent, persist it, and reuse it across transport retries. Transport material such as a server challenge may change, but the business ID must not. The server treats (subjectId, operationId) as a uniqueness boundary and stores a canonical payload hash with it. PostgreSQL unique constraints enforce row uniqueness, while INSERT ... ON CONFLICT provides an atomic conflict path under concurrency.

SQL
 
INSERT INTO agent_operation(subject_id, operation_id, payload_hash, status)
VALUES (:subject, :operationId, :payloadHash, 'ACCEPTED')
ON CONFLICT (subject_id, operation_id) DO NOTHING;


A conflict with the same payload hash returns the existing operation; a different hash rejects key reuse. The record should exist before agent execution starts, and the accepted response should expose the durable operation identity.

Let LangGraph Resume Without Repeating Effects

LangGraph persistence is useful precisely because durable execution can replay code. With a checkpointer, LangGraph saves state at super-step boundaries; if execution resumes after a failure, an affected node can run again from the beginning. Official guidance consequently requires idempotent node logic, and task results can be checkpointed so completed task work can be reused during resume instead of recomputed. Replaying from an earlier checkpoint can also re-trigger later LLM calls and API requests. 

A stable business operation should therefore map to a stable LangGraph thread, while every effectful tool boundary receives the same operation ID.

Python
 
config = {"configurable": {"thread_id": operation_id}}

result = graph.invoke(
    {"operation_id": operation_id, "command": command},
    config
)


Checkpointing reduces recomputation but does not replace downstream idempotency. A reservation can succeed before its task result is durably checkpointed. LangGraph’s functional API therefore recommends idempotent tasks because an incomplete task can execute again during resume.

Python
 
@task
def reserve_inventory(operation_id, sku, quantity):
    return mcp.call_tool("reserve_inventory", {
        "operationId": operation_id,
        "sku": sku,
        "quantity": quantity
    })


The significant property in this snippet is not the decorator. The important part is that the business identity crosses the graph boundary and reaches the tool implementation. A downstream inventory service can then use that identity to return a previously committed reservation rather than creating another one.

MCP Tasks Are Durable Handles, Not Deduplication Keys

The current MCP Tasks design is especially relevant to long-running agent tools. In the July 28, 2026 protocol revision, Tasks moved into the io.modelcontextprotocol/tasks extension. A server can return a durable task handle, and the client can poll with tasks/get, provide input with tasks/update, or request cancellation with tasks/cancel. The task is durably created before its handle is returned, which allows polling after a disconnect. 

That durability solves result retrieval after task creation, but it does not by itself deduplicate the request that creates the task. The task ID is server-generated. If the server creates task A, the response disappears, and the original tools/call is sent again, a naïve implementation can create task B. Therefore, the business operationId must be part of the tool arguments or equivalent application metadata, and task creation must first look up an existing operation. This follows directly from MCP’s server-generated task-ID model combined with retry ambiguity at the HTTP boundary. 

The MCP server can return an existing task handle for the same authenticated subject, operation ID, and payload hash, and later return the stored terminal result. Cancellation should also be idempotent because MCP defines it as cooperative rather than a guarantee that underlying work stops immediately. 

Keep Kafka Guarantees Inside Kafka

Kafka is most valuable after the operation has been claimed. A database transaction can persist operation state with an outbox row carrying the same ID. Kafka producer idempotence protects against duplicates caused by producer retries, while consumers can still use the operation ID for application-level deduplication. Kafka transactions can atomically cover Kafka writes, but they do not extend over an MCP server or payment API. 

The event contract should preserve causality rather than inventing a new identity at each hop.

JSON
 
{
  "operationId": "8E7B6D9E-...",
  "type": "AgentToolCompleted",
  "tool": "reserve_inventory",
  "status": "SUCCEEDED"
}


A consumer can enforce uniqueness on (consumerName, operationId, eventType) or make the state transition conditional. Kafka delivery guarantees and application idempotency then reinforce each other instead of being treated as interchangeable.

Bind Retry Identity to App Attest Without Blocking Legitimate Retries

App Attest addresses a different failure mode: whether a request comes from a legitimate app instance and whether signed request material has been replayed or altered. Apple’s current guidance uses a server-provided challenge for assertions and requires the server to validate a strictly increasing assertion counter; that counter is specifically an anti-replay signal. Assertions are generated locally on the device after key attestation. 

The App Attest assertion must not become the business idempotency token. A legitimate retry should obtain fresh challenge material and generate a fresh assertion while retaining the original operation ID. The data hashed for the assertion can bind the server challenge, operation ID, and canonical payload hash together.

Swift
 
let payloadHash = SHA256.hash(data: body)
let clientData = challenge + operationID.data + Data(payloadHash)
let clientDataHash = Data(SHA256.hash(data: clientData))

let assertion = try await service.generateAssertion(
    keyID,
    clientDataHash: clientDataHash
)


Apple recommends server-controlled challenges, server-side validation, and assertion-counter tracking as assertions are generated on demand without a round trip to Apple’s servers.  The server verifies App Attest, checks that the challenge binds the operation ID and payload, then performs the idempotency lookup. A fresh assertion can retry the same operation; a replayed assertion fails anti-replay validation; an altered payload fails the hash check.

Effectively-Once Behavior Is a Composition Property

Reliable agent execution does not come from asking iOS to retry less often or from labeling a Kafka pipeline “exactly once.” It comes from carrying one durable business identity across every retry and every boundary, claiming that identity atomically before execution, making LangGraph effects idempotent under resume, using MCP Tasks as durable result handles rather than creation-time deduplication keys, restricting Kafka’s exactly-once guarantees to Kafka’s transactional domain, and using App Attest to prove request integrity without confusing anti-replay state with business deduplication. 

When those boundaries align, a lost mobile response can cause another HTTP attempt, another graph invocation, or another poll, but it does not cause another business effect. That is the operational meaning of effectively once.

API kafka workflow AI

Opinions expressed by DZone contributors are their own.

Related

  • Event-Driven AI Systems With Kafka and Autonomous Agents
  • Prompt Caching: Overriding Tokenization for Faster and More Cost-Effective AI
  • Handling Large API Responses Without Freezing the Client: A Practical Architecture With Temporal, Kafka, and RAG
  • How to Detect AI-Generated Images in C# Using an API

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook