Your AI Agent Needs a Cancellation Contract, Not Just a Stop Button A developer argues that AI agents need a durable cancellation contract rather than a simple stop button, proposing a state machine with cancel versions and compare-and-set operations to prevent resurrected work and reconcile side effects. The approach separates cancellation from process liveness and requires checks at every work-creating boundary. A stop button is not a cancellation protocol. In a toy agent, “stop” can mean setting a boolean and waiting for the loop to exit. In a real agent, work may already be queued, claimed by another worker, inside a browser session, or waiting for an outbound side effect. If cancellation is not represented as durable state, a restart can resurrect work the operator thought they stopped. The useful question is not “did the process receive SIGTERM?” It is: Can every layer prove whether this run may still start work, whether in-flight work must finish, and what happened to side effects that were interrupted? This article turns cancellation into a small contract you can test. Keep cancellation separate from process liveness. A worker can be alive while its run is cancelled, and a worker can die before it records the cancellation. A minimal run state machine is: Store the state durably with a monotonically increasing cancel version: run id status cancel version updated at run 42 CANCELLING 3 2026-08-19T12:00:00Z Workers must carry the version they observed. A dispatch is valid only if the durable row still says ACTIVE with the same version. This closes the race where an operator clicks Stop after a worker checked the run but before it starts a tool call. A cancellation check only at the top of the agent loop is too weak. Check the contract at each boundary that can create work: That last distinction matters. Cancelling a code-generation run does not automatically prove that an already-created notification was unsent. Execution and delivery need separate records. Cooperative cancellation is the default: the worker notices the state change at safe checkpoints and exits cleanly. Forced cancellation is a deadline for the worker that does not cooperate. A practical sequence is: php ACTIVE - CANCELLING revoke admission and retries - drain safe checkpoints - CANCELLED if no in-flight effects remain - UNKNOWN if an external effect cannot be reconciled Do not mark a run CANCELLED merely because the worker process exited. A process can die after sending a request and before recording the response. For every external side effect, record an intent with a stable key before dispatch, then reconcile UNKNOWN using the provider’s lookup API, webhook, or an operator decision. A cancellation timeout should transition the run to UNKNOWN or CANCELLING TIMEOUT, not silently to success or cancellation. That makes the ambiguity visible instead of converting it into duplicate work on restart. The critical operation is a compare-and-set, not a read followed by a write: UPDATE runs SET status = 'CANCELLING', cancel version = cancel version + 1, updated at = CURRENT TIMESTAMP WHERE run id = :run id AND status = 'ACTIVE'; A worker dispatch can then require the exact version it observed: UPDATE steps SET status = 'DISPATCHED', dispatch version = :cancel version WHERE step id = :step id AND status = 'CLAIMED' AND EXISTS SELECT 1 FROM runs WHERE run id = :run id AND status = 'ACTIVE' AND cancel version = :cancel version ; If the update affects zero rows, the worker must not call the tool. It should release the claim and record CANCELLED BEFORE DISPATCH. A cancellation feature is incomplete until it survives these injected failures: | Failure | Expected evidence | |---|---| | Cancel between queue claim and dispatch | No tool request, or a reconciled effect record | | Worker pauses after the state check | Stale version is rejected at dispatch | | Process dies after provider request | Effect becomes UNKNOWN, then reconciles | | Retry timer fires after cancellation | Retry is rejected and recorded | | Cancellation store is unavailable | Fail closed for new effects; preserve the run as unresolved | | Browser action is mid-flight | No next mutation; current action is explicitly unresolved | | Controller restarts during drain | Durable CANCELLING state resumes the drain | For each case, assert both safety and evidence: no unauthorized new effect, no lost cancellation intent, and a record an operator can explain later. If you run an always-on OpenClaw or browser agent, a managed runtime such as managed OpenClaw hosting on Ampere https://ampere.sh/?utm source=devto&utm medium=article&utm campaign=cancellation-contract can be one deployment option to evaluate. It does not replace durable run state, fencing, credential scope, or reconciliation. Those remain properties of the agent control plane. Before trusting a Stop button, verify that: The practical goal is not instant termination. It is a system that can prove what was prevented, what was already in flight, and what still needs reconciliation. That is the difference between a UI button and an operational cancellation contract. If you are building coding agents or automation that must survive restarts and operator intervention, follow for more concrete control-plane tests and failure drills.