# Your AI Agent Needs a Cancellation Contract, Not Just a Stop Button

> Source: <https://dev.to/zira125/your-ai-agent-needs-a-cancellation-contract-not-just-a-stop-button-4k8n>
> Published: 2026-08-19 19:43:13+00:00

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.
