DEV Community

Zira
Zira

Posted on

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

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.

Define cancellation as a state machine

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:

  • ACTIVE: new work may be admitted.
  • CANCELLING: no new work may start; in-flight work is being observed or stopped.
  • CANCELLED: the run will not resume and no unclaimed step may dispatch.
  • COMPLETED: the intended work finished.
  • UNKNOWN: the controller cannot prove whether an external effect happened.

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
Enter fullscreen mode Exit fullscreen mode

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.

Make cancellation explicit at every boundary

A cancellation check only at the top of the agent loop is too weak. Check the contract at each boundary that can create work:

  1. Admission: reject newly submitted steps for a cancelled run.
  2. Queue claim: do not claim a step whose run is CANCELLING or CANCELLED.
  3. Tool dispatch: atomically recheck run state, cancellation version, policy, and credential lease.
  4. Retry scheduling: cancellation revokes future retries, not just the current attempt.
  5. Browser actions: stop before the next navigation or mutation, but record that a page action may already be in flight.
  6. Outbound delivery: apply the delivery policy independently from execution cancellation.

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.

Use cooperative and forced cancellation together

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:

ACTIVE
  -> CANCELLING (revoke admission and retries)
  -> drain safe checkpoints
  -> CANCELLED (if no in-flight effects remain)
  -> UNKNOWN (if an external effect cannot be reconciled)
Enter fullscreen mode Exit fullscreen mode

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.

A small implementation sketch

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';
Enter fullscreen mode Exit fullscreen mode

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
  );
Enter fullscreen mode Exit fullscreen mode

If the update affects zero rows, the worker must not call the tool. It should release the claim and record CANCELLED_BEFORE_DISPATCH.

Test the races, not just the button

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.

Hosting does not define cancellation semantics

If you run an always-on OpenClaw or browser agent, a managed runtime such as managed OpenClaw hosting on Ampere 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.

The checklist

Before trusting a Stop button, verify that:

  • cancellation is durable and versioned;
  • admission, claim, dispatch, retry, and delivery each recheck it;
  • workers cannot dispatch with a stale cancellation version;
  • cooperative drain has a bounded forced-cancellation path;
  • external effects have stable keys and an UNKNOWN outcome;
  • restart resumes from durable cancellation state;
  • tests inject races at every side-effect boundary.

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.

Top comments (0)