Skip to main content

The Dual-Writer Race: When Your Agent and Your User Edit the Same Calendar Event

· 12 min read
Tian Pan
Software Engineer

The agent confidently reports "I rescheduled the meeting to Thursday at 3pm." The user is staring at the original Tuesday 10am slot, because between the agent's plan and its commit they edited the event themselves. Last-write-wins overwrote a human change with an automated one, and the user's trust in the assistant collapses on a single incident. This is the dual-writer race, and it is the bug class that agent toolchains were never designed for.

Most agent platforms inherit this category by accident. The tool layer treats update_event as a single function call: take an ID, take new fields, return success. The provider API underneath has offered optimistic concurrency primitives — ETags, version tokens, If-Match preconditions — for a decade, and almost nobody plumbs them through. The model has no way to know that the world it reasoned about a minute ago is no longer current, because the abstraction it was given silently throws that information away.

The pattern shows up everywhere agents touch shared state: calendars, CRM records, Jira tickets, shared docs, project trackers, ticketing queues. The user opens a record a second after the agent reads it, both edit, the agent writes last, and the human's change vanishes. The agent reports success. The dashboard says success. The user disagrees, and there is no apology message in the prompt template that fixes it.

Why The Tool Layer Drops The Version Token

Walk through what a typical "reschedule meeting" tool looks like in production. The agent calls get_event(event_id), the SDK returns a parsed object with summary, start, end, attendees, location. The agent reasons over those fields, picks a new time, calls update_event(event_id, {start, end}). The wrapper does a PATCH against the provider. Done.

Notice what is missing. The provider response carried an etag field — Google Calendar exposes one on every event resource, and Microsoft Graph exposes @odata.etag on every event. Those tokens identify the version the agent saw. The SDK either dropped them on the floor during deserialization, or kept them but the tool wrapper never read them, or read them but the PATCH request never set If-Match. By the time the call leaves your gateway, the request says "set this event to these fields, regardless of what state it's in." Last-write-wins is not a default the agent chose; it is a default the tool layer baked in.

The fix at the protocol level is mechanical. Google Calendar accepts If-Match: <etag> and returns 412 Precondition Failed when the resource has changed since the read. Microsoft Graph requires If-Match on PATCH/DELETE for several resource types and returns 409 Conflict when the ETag is stale — Planner enforces this strictly, and a recent practitioner walkthrough on connecting AI agents to Planner notes the rule explicitly: "always fetch immediately before update, never cache ETags." Jira's transition API switched its concurrent-transition response from 400 to 409 Conflict for the same reason: 409 is the standard signal that "your write lost a race."

The version token is the contract the provider gives you for free. The tool layer's job is to keep it intact end to end.

The Three-Layer Fix: Read-With-Version, Write-With-Precondition, Conflict-As-Outcome

A dual-writer-safe tool needs three things working together, and skipping any one of them silently re-introduces the race.

Read-with-version. Every read tool returns the object plus the version token, and the agent's working memory keeps them paired. If your tool schema today returns {event: {...}}, change it to return {event: {...}, version: "abcd1234"} and document the version field as opaque-but-required-for-writes. The agent does not need to understand the token; it just needs to thread it back through.

Write-with-precondition. Every write tool accepts a version token and forwards it to the provider as If-Match (or whatever the provider's equivalent is — IfMatch parameter on Microsoft Graph SDKs, If-Match header on Google APIs, version payload field on Jira issues, the _token you stored from the prior SELECT FOR UPDATE on Salesforce). If the agent calls write without a version, the tool refuses — the failure mode "agent forgot to pass the token" should be loud, not silent.

Conflict-as-outcome. When the precondition fails, the tool does not retry. It returns a structured outcome to the planner: "conflict: the event was modified between your read and your write; the new version is X." That outcome has to be a first-class result the model can reason over, not a hidden retry that re-reads, re-applies, and writes again. The hidden retry is the worst design choice, because it converts "the human's change matters" into "the human's change quietly disappears" — exactly the failure the version token was meant to prevent.

The reason the third layer matters is that an agent told only "the write failed, try again" will helpfully re-read, re-apply the same intent, and overwrite the human anyway. The agent has to know that the world changed so the planner can decide whether the original intent still makes sense. A meeting reschedule whose underlying event the user just deleted is no longer a reschedule; it is a different decision.

What Conflict Resolution Looks Like When You Can't Just Retry

Suppose the agent planned to move a 30-minute weekly sync from Tuesday 10am to Thursday 3pm. Between read and write, the user accepted a different time on the same event from someone else's invite, and the event is now Tuesday 11am. The 412 fires. What does the agent do?

Three honest paths exist, and the right one depends on the tool's contract with the user.

The first is re-plan with fresh state. The conflict outcome includes the new version of the event. The planner re-reads the event, sees the user's change, and asks the model whether the original intent (move to Thursday 3pm) still applies given the new starting state. Often it does — the user changed the time, but the goal of moving to Thursday is unchanged. Sometimes it does not — the user already moved the meeting themselves, and the agent's job is now to do nothing.

The second is surface the conflict to the user. The tool returns to the planner, which generates a message: "I was about to move your Tuesday 10am sync to Thursday 3pm, but I noticed you just changed it to Tuesday 11am — should I still move it, or leave it where you put it?" This is the right path when the agent's confidence in re-planning is low, or when the user-facing UX has space for a clarifying turn. It is the path that builds trust, because admitting the conflict is what proves the agent saw it.

Loading…
References:Let's stay in touch and Follow me for more thoughts and updates