
After the Stop Button: Cancellation Semantics for AI Agents
A field guide to propagating cancellation, proving work has stopped, and reconciling side effects when an AI task outlives the user's intent.
Read MoreZharfAI Team

Two operations agents receive different alerts about the same production service. One decides to roll the service back to the last known-good release. The other validates a hotfix and decides to promote it. Both agents are competent. Both hold valid tool permissions. Both read a plausible version of the deployment state. If they act at nearly the same time, the later packet—not the better decision—may determine what runs.
This is a concurrency failure, not a reasoning failure. A model can explain its plan perfectly and still write against state that changed after the plan began. A lease can expire while a slow worker continues. A new leader can take over while a delayed command from the former leader is still in flight. A database row, object, ticket, branch, invoice, reservation, or physical controller can therefore receive two individually valid but jointly incompatible actions.
The operating decision is: for each shared resource, should workers partition ownership, serialize through one writer, compare and swap against a version, commit inside a transaction, hold a lease with a fencing token, or produce mergeable proposals instead of direct writes? The choice belongs in application architecture. It must not be improvised by a language model after contention begins.
A retry repeats one logical intent because its outcome is uncertain. Concurrency means two or more live intents overlap on the same invariant. The retry and idempotency guide prevents one intent from becoming several effects. It does not decide which of two different intents should win.
Cancellation is also different. A stop request changes future intent, but an old worker may continue long enough to send a late write. The cancellation semantics guide explains why loss of interest or loss of a lease is not proof of quiescence. Concurrency control makes the target reject an action whose authority or expected state is no longer current.
Three claims must remain separate:
Only the third claim closes the stale-writer path. A distributed lock that the target never checks is an advisory story, not an enforcement boundary.
HTTP already carries a useful optimistic-concurrency primitive. RFC 9110 defines If-Match for state-changing requests: the origin server evaluates a strong entity tag before performing the method and normally returns 412 Precondition Failed when the representation has changed. The specification describes this as protection against the lost-update problem. It does not decide how an AI workflow should resolve the conflict; it gives the application an honest refusal instead of a silent overwrite.
The Amazon DynamoDB concurrency guidance distinguishes optimistic version checks for low contention, transactions for atomic multi-item changes, and a lease-and-heartbeat lock client for long-running coordination. It also warns that global tables use last-writer-wins reconciliation, so a version pattern does not provide the same protection across Regions. The datastore's replication contract matters as much as the code around it.
At the database level, PostgreSQL 18 transaction-isolation documentation says successfully committed Serializable transactions have an effect consistent with running one at a time. When PostgreSQL detects a serialization anomaly, an application must be prepared to retry the entire transaction. The transaction must contain the integrity-critical read and write; reading, asking a model for a minute, and later opening a new transaction does not preserve the original snapshot.
Coordination systems address longer ownership. Kubernetes Leases are used for node heartbeats and component leader election so one active instance can perform control work while peers stand by. Apache ZooKeeper's lock recipe uses ephemeral sequential nodes, watches only the next predecessor to avoid a herd, and attaches a GUID so a client can discover whether a create succeeded when the response was lost.
Those mechanisms help select an owner. They do not, by themselves, stop a former owner from reaching an external target late. Google's original Chubby lock-service paper makes the missing step explicit: a lock holder can pass a sequencer containing the lock generation to the server it calls, and the server rejects an invalid or older sequencer. This is the fencing pattern. Ownership becomes a value the effect target can compare, not merely a belief held by the worker.
Object storage exposes the same principle without calling it a lock. Google Cloud Storage request preconditions let a write or delete require an exact object generation. A delayed delete aimed at an older generation then fails rather than removing a newly created object with the same name. Preconditions are useful because they bind the action to the object instance the caller actually observed.
These are verified protocol behaviors. The operating model below is ZharfAI analysis that composes them for tool-using AI systems.
Do not put a distributed lock around everything. First name the resource, invariant, contention pattern, and consequence.
| Contract | Use when | Commit rule | Main trade-off |
|---|---|---|---|
| Partition ownership | Work can be divided by tenant, account, document, region, or immutable artifact | Only the assigned partition owner may write | Rebalancing and cross-partition actions are harder |
| Single writer or queue | Order matters and throughput is bounded | One sequencer commits intents in a declared order | Queue delay and writer availability |
| Compare and swap | Conflicts are infrequent and one resource version captures the invariant | Commit only if expected_version == current_version | Conflicts require reread and replanning |
| Serializable transaction | Several database reads and writes must appear atomic | Commit all integrity-critical changes or abort | Retries, contention, and transaction scope |
| Lease plus fencing | Work is long-running or reaches an external system | Target accepts only a fencing token newer than any it has accepted | Every effect path must enforce the token |
| Proposal and merge | Outputs can remain branches, drafts, patches, or sets until reviewed | Merge through a deterministic or human-owned gate | Direct automation is delayed |
Partitioning is usually the cheapest safe answer. A worker processing account A should not contend with one processing account B. A single writer is appropriate when a deployment, ledger, schedule, or finite inventory has one meaningful order. Compare-and-swap fits short read-modify-write operations. Transactions fit invariants that live inside one transactional boundary. Leases with fencing fit work that cannot stay inside a database transaction. Proposal-and-merge is the safe default when automatic conflict resolution would erase meaning.
The admission-control field guide is relevant here: serialization moves contention into a queue, so the queue still needs limits, expiry, fairness, and a clear rejection state. Concurrency safety must not create an unbounded warehouse of obsolete work.
Every consequential write should carry machine-derived concurrency fields alongside the business arguments:
| Field | Why it exists |
|---|---|
| Logical intent ID | Joins planning, retries, approvals, and final evidence |
| Canonical resource ID | Defines exactly what may be changed |
| Expected resource version | Makes stale reads fail instead of overwrite |
| Owner or worker ID | Identifies the actor for diagnosis, not authority by itself |
| Lease ID and expiry | Bounds coordination and supports takeover |
| Fencing token | Monotonically orders owners at the effect target |
| Operation digest | Binds approval and commit to canonical arguments |
| Policy and release version | Preserves the rules and software assembly in force |
| Commit result ID and new version | Proves which state the target accepted |
Generate these fields in trusted application code. The model may identify a likely record from natural language, but the product must resolve that suggestion to a canonical resource under authenticated scope. The model must not invent a version, widen a lock key, extend its own lease, or decide that two resources are “close enough” to share a token.
Bind the concurrency envelope to the release described in the AI release passport. A worker running old adapter code may format a fencing token incorrectly or omit a version precondition. Release identity therefore belongs in the same evidence chain as resource identity.
The guard must sit where the irreversible or authoritative change occurs. If an orchestrator checks a token and then calls an unguarded vendor API, a delayed worker can bypass the guarantee through another path.
For a database row, enforce a version predicate in the update or use a transaction. For an object, require a generation or ETag precondition. For a deployment controller, store the highest accepted fencing token per service and reject a lower one. For an external API without concurrency support, place a single controlled adapter in front of it; serialize calls there, reconcile authoritative state, and never expose the vendor credential directly to workers.
Keep the model call outside a database transaction and usually outside the shortest lock. Take a snapshot, plan, validate, then reacquire or verify current state immediately before commit. If the state changed, the plan is stale even when its prose remains sensible. Re-read, re-evaluate policy, and either replan or surface a conflict.
Do not automatically retry a 409, 412, failed condition, or serialization error with the same old arguments. A concurrency refusal is new information. The correct response may be to recompute, merge, abstain, ask a human, or declare the intent superseded.
Assume a deployment controller stores release=R12, version=41, and highest_fence=781.
782, reads version 41, and prepares a rollback to R11.783, reads the current service, validates hotfix R13, and commits with expected_version=41 and fence=783.release=R13, version=42, and highest_fence=783.expected_version=41 and fence=782.The controller rejects A for two independent reasons: the resource version changed and the fencing token is older than the highest accepted token. A records STALE_OWNER, fetches version 42, and routes the original incident intent for reassessment. It does not “helpfully” retry the rollback against R13.
If the controller checked only whether A once held the lease, the stale rollback could win. If it checked only the expected version, a different operation that does not change that version might still expose a gap. The target should validate the strongest invariants it owns, atomically with the effect.
Many agent actions cross a database, a ticketing system, object storage, and a vendor API. No single lock makes those systems one transaction. Define a canonical order and a state machine such as:
PROPOSED: plan exists but owns nothing;CLAIMED: a bounded owner and fencing token exist;PREPARED: current versions and policy checks pass;COMMITTING: the guarded target action is in flight;COMMITTED: authoritative result and new version are recorded;CONFLICTED: a precondition or serialization guard refused the action;STALE_OWNER: a newer fencing token exists;RECONCILIATION_REQUIRED: outcome or cross-system state is uncertain.Use an effect ledger for every external commit. If step two fails after step one succeeds, do not hold a fake global lock and call the workflow atomic. Record the partial state, stop incompatible successors, and compensate or reconcile through the same discipline described in the cancellation guide.
The remedy is not more chain-of-thought. It is smaller ownership scope, guarded commits, and explicit conflict states.
Build deterministic tests that pause workers at every boundary: after read, after lease acquisition, after planning, before commit, after target acceptance, and before acknowledgement. Then inject duplicate delivery, response loss, worker crash, lease expiry, coordinator failover, delayed packets, message reordering, clock skew, and two aliases for the same resource.
Assert invariants:
Monitor conflict rate, serialization retries, lease turnover, stale-writer rejection, lock wait, queue age, fencing enforcement coverage, manual-merge rate, reconciliation age, and commits without a recorded precondition. Slice by resource class and workflow. A low global conflict rate can hide one high-consequence resource whose lock key is wrong.
Preserve enough evidence to prove the claim. The audit-evidence guide shows the broader method: log the assertion an investigator must verify, not every token the model produced.
Before several AI workers can change shared state, answer yes:
Parallel agents are useful because they reduce elapsed time and bring specialized judgment to a workflow. They are dangerous when concurrency is treated as a scheduling detail. The reliable design gives each shared invariant one enforceable commit order—and makes stale authority fail at the place where failure still prevents harm.
If-Match, If-None-Match, lost-update prevention, and 412 Precondition Failed behavior.
A field guide to propagating cancellation, proving work has stopped, and reconciling side effects when an AI task outlives the user's intent.
Read More
An operating guide for deciding when an AI call may be retried, hedged, failed over, or reconciled—and for preventing timeout ambiguity from becoming duplicate business action.
Read More
A field guide to workload identity, token exchange, scoped credentials, connector proxies, and evidence when AI agents need temporary access to enterprise systems.
Read MoreIf this note maps to a real system in your organization, start with the services page or a shipped case study.