The Dual-Writer Race: When Your Agent and Your User Edit the Same Calendar Event
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.
The third is abort and explain. The agent reports that it could not complete the action because the underlying record changed, and stops. This is the right path for write-once flows (sending an invite, transitioning a ticket past a one-way gate) where re-planning is meaningless and surfacing the conflict would create more confusion than value.
What does not work is a silent retry policy in the tool layer that catches 412/409 and quietly re-runs. That pattern was inherited from idempotent web requests where the world had not changed underneath the call — but in a dual-writer scenario, the precondition failed because the world changed, and re-running with stale intent is the bug, not the fix.
The Other Half: Knowing The State Changed Before You Tried To Write
Optimistic concurrency catches the conflict at write time. That is too late for a different failure mode: the agent reasons for thirty seconds across multiple tool calls, and by the time it commits, the user has already done the thing it was about to do. The PATCH succeeds — the version token still matched, because the user moved a different event — but the agent's whole plan was based on a world that no longer exists.
The defense here is a change-feed subscription. Google Calendar offers push notifications where the API hits a webhook with X-Goog-Resource-State headers when a watched calendar changes. Microsoft Graph offers delta queries that return only what changed since a sync token. The agent's session subscribes to the resources it touches at the start of a multi-turn task; when a notification fires for a record the agent already read, the runtime invalidates that record in working memory and surfaces "this changed under you" to the planner.
In practice this is a session-state problem more than an API problem. The agent's working memory needs a "live read set" — the IDs and versions of every resource it has loaded — and a hook that fires when any of them is invalidated. Most agent frameworks today have no such concept; they treat every tool call as independent and assume the world is stationary between calls. Adding the live read set is unglamorous plumbing, but it is what makes the agent fail loudly on stale plans instead of confidently shipping them.
A weaker but cheaper version of the same idea is a re-read pass before any write that depends on more than thirty seconds of reasoning. If the planner is about to commit and the read happened a long time ago, do another read first, diff against what was loaded, and bail out to re-planning if the diff is non-trivial. This catches the slow-reasoning case without requiring webhooks.
Eval Has To Inject The Conflict, Not Wait For It
The reason this bug class survives all the way to production is that evals never produce it. The fixture environment is single-writer by construction: the eval harness creates the test event, the agent reads it, the agent writes it, the assertion checks the final state. No human edits the event mid-run, because there is no human in the eval. The agent passes. The behavior in production is identical until the day a real user touches a real record at the wrong moment.
Adversarial concurrency has to be a first-class eval scenario. The harness needs at least three test shapes:
- Mid-tool-call edit. The harness modifies the resource between the agent's read and the agent's write. Pass condition: the agent either re-plans correctly or surfaces the conflict — never reports success without acknowledging the change.
- Stale plan over fresh state. The harness modifies the resource after the agent's last read but before its commit, in a way that makes the planned action wrong. Pass condition: the agent detects via change feed or pre-commit re-read.
- Already-done action. The harness performs the action the agent was about to perform (the user already moved the meeting; the user already closed the ticket). Pass condition: the agent recognizes the state and does nothing instead of doing it again.
Recent academic work on collaborative document editing with multiple users and AI agents frames the same shape of problem in the document setting: the moment you assume there is more than one writer, the eval set has to model the others, or the system you ship will assume it is alone.
Why CRDTs Are A Distraction For This Class Of Bug
When concurrency comes up, CRDTs and operational transformation enter the conversation. They are the right tools for real-time character-level co-authoring — the Google Docs / Figma class of problem — where two writers are typing into the same document simultaneously and the system has to merge intents at sub-second granularity. Figma famously moved from OT to CRDTs, at the cost of six months of engineering, to get convergence guarantees on shared canvas state.
Almost no agent dual-writer scenario looks like this. The agent is editing structured records — events, issues, contacts, deals — at second-to-minute timescales, against APIs that already expose ETags and version tokens. Reaching for a CRDT here is over-engineering for a problem the protocol already solved. The fix is plumbing the existing primitives through the tool layer, not introducing a distributed data structure to a system that has a perfectly good central authority. Save CRDT-shaped solutions for the moments your agent and your user are literally typing into the same paragraph at the same time.
The Org Failure That Lets This Ship
The reason this bug class is endemic is organizational, not technical. The team that builds the agent tool layer optimizes for "the call succeeds." The team that owns the integration with the calendar provider treats the SDK as a black box. The team that runs evals tests the happy path. The team that handles incidents has no shared vocabulary with any of the above for "the agent's write was technically successful but semantically wrong because it overwrote a human."
Putting "dual-writer-safe" on the platform team's roadmap is the move. It needs three deliverables: a tool-schema standard that requires version tokens on read and accepts them on write, a conflict-as-outcome convention in the planner so the model can actually reason about a 412/409, and an eval harness with adversarial concurrency baked in. None of these are research problems. They are infrastructure problems that have been solved in REST API design for fifteen years and are waiting for someone to walk them across the agent boundary.
The agents that will earn lasting trust are not the ones with the cleverest reasoning. They are the ones whose tool calls fail loudly and accurately when the world moved, and whose user-facing surface admits the conflict instead of papering over it. "I was about to move your meeting, but I noticed you already did" is a sentence that makes an assistant feel alive. "I rescheduled the meeting" — said over a calendar that did not change — is the sentence that makes one feel broken.
- https://developers.google.com/workspace/calendar/api/guides/version-resources
- https://developers.google.com/workspace/calendar/api/guides/push
- https://developers.google.com/workspace/calendar/api/guides/errors
- https://learn.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-1.0
- https://learn.microsoft.com/en-us/graph/delta-query-events
- https://developer.atlassian.com/cloud/jira/platform/change-notice-update-in-simultaneous-transitions-issue-api/
- https://www.nz365guy.com/blog/connecting-ai-agents-to-microsoft-planner
- https://www.salesforceben.com/salesforce-record-locking-tips-for-developers-how-to-avoid-concurrency-issues/
- https://www.tiny.cloud/blog/real-time-collaboration-ot-vs-crdt/
- https://arxiv.org/abs/2509.11826
