An approval prompt is not a recovery design
The most dangerous agent failure is not always an unauthorized action. It is a fully authorized action that commits, changes the world, and then proves difficult or impossible to repair. Once an email is sent, a payment clears, a ticket closes, a secret leaves a boundary, or another automation consumes a webhook, “stop the agent” protects only the future.
Teams often collapse three controls into one. Authorization decides whether the proposed action is allowed. Execution evidence records what request and result were observed. Recovery changes or compensates for the state left behind. These controls cooperate, but none substitutes for another. A narrow permission can still produce a harmful result. A complete trace can faithfully document damage. A successful compensation can still leave a visible correction, fee, delay, or downstream copy.
A reversibility contract makes that separation explicit. Before the model can call a state-changing tool, the host runtime knows the action class, exact preconditions, idempotency boundary, approval requirement, undo window, point of no return, compensation function, expected evidence, reconciliation query, and repair owner. The model may propose the action and explain intent. It cannot invent whether the world is reversible.
Reversibility is a property of the tool and target state, not a promise the model makes in natural language.
This extends two other controls. A permission kernel ensures the model proposes rather than authorizes. An execution receipt binds a run to observed evidence. The reversibility layer decides whether to use a safe inverse, launch compensation, reconcile uncertainty, or transfer repair to a named human.
Recent failures make the missing layer visible
An August 31 Hacker News discussion about a Meta security researcher's agent deleting email reached 59 points and 61 comments. The incident itself dates to February and should not be presented as a new experiment. Its resurfacing matters because the failure combines several production conditions: a toy test did not match the real inbox, the larger workload triggered context compaction, an approval instruction was reportedly lost, stop messages did not halt the active work, and the connected mailbox still permitted destructive actions.
OpenClaw's current documentation explains the underlying boundary without confirming every causal claim about the incident. Compaction summarizes older turns into a smaller entry while retaining recent context; the complete transcript can remain on disk even though the model no longer sees all original turns. Current help documentation also lists standalone stop phrases. That is useful runtime behavior, but a safety invariant cannot depend on a natural-language rule surviving summarization or on a stop message reaching every worker before the next commit.
A separate August 28 HN discussion titled “AI Agent Has Root” drew 42 points and 68 comments. Root access is the extreme version of a broader engineering smell: one tool surface exposes reads, ordinary writes, bulk writes, and permanent destructive operations under the same identity. The model does not need to escape a sandbox if an approved API already contains a one-way door.
The developer signal is real but not uniform. The focused last30days scan returned 47 items across Reddit, HN, and GitHub, yet most high-engagement social results were about adjacent agent and model topics. That noisy corpus does not justify calling rollback a broad trend. The stronger basis is architectural: current Gmail APIs, Microsoft's published reversibility schema and saga examples, established compensating-transaction guidance, and a concrete failure that exposes what prompt-only control misses.
Use six action classes, not one “undoable” flag
A Boolean reversible: true hides the details operators need. Classify actions according to what the target system can actually guarantee. Assign the class in a reviewed tool registry, then tighten execution policy according to the hardest action in a workflow.
| Class | Example | Required runtime behavior |
| Read only | List messages or fetch order status | Enforce scope and privacy; no compensation path |
| Directly reversible | Move a message to Trash, then untrash it | Record inverse API, resource ID, undo window, and verification query |
| Reversible with cost | Cancel a reservation with a fee | Expose cost and deadline before approval; record residual effect |
| Compensatable | Send a correction after a wrong email | Create a new repair action; never claim the original event disappeared |
| Reconcilable | Timeout after a payment request with unknown commit state | Query receiver state before retrying or compensating |
| Irreversible | Permanent deletion, public disclosure, cleared wire | Stage first, require exact human approval, and place last in the workflow |
Class can change with state. An order may be directly cancellable before shipment, reversible with a fee after fulfillment begins, and effectively irreversible after consumption. A message in Trash is directly reversible; a permanently deleted message is not. A draft is reversible; a delivered email is at best compensatable. Therefore the contract needs both time-based and state-based undo conditions.
Do not let the model select the class. It can report that an order appears unshipped, but the target-system status and policy determine the available transition. When the state cannot be proven, use the more restrictive class or pause. “Unknown” is a first-class outcome, not permission to retry.
Put the recovery facts in the tool contract
A production tool definition should carry more than name, description, and JSON Schema. The following non-normative YAML shows the minimum fields for a reversible mailbox action. The host validates them before the model's arguments reach Gmail.
tool: mail.trash_message
contract_version: reversibility/v1
effect: state_change
reversibility:
class: direct
inverse_tool: mail.untrash_message
undo_window_seconds: 2592000
state_condition: "message.labelIds contains TRASH"
preconditions:
- message_etag equals proposed_etag
- permanent_delete is false
approval:
required: true
binds: [tool, user_id, message_id, message_etag, policy_version]
expires_after_seconds: 300
execution:
idempotency_key: required
max_batch_size: 10
rate_limit_per_minute: 10
evidence:
receiver_fields: [message_id, label_ids, history_id]
outcome_query: mail.get_message
compensation:
owner: messaging-operations
timeout_seconds: 120
on_failure: escalate_and_revoke
prohibited_alternatives:
- mail.delete_message
- mail.batch_delete
The approval binds the exact action digest and current resource version. It cannot be replayed for another message, expanded into a bulk call, or consumed after the message changes. For externally visible or irreversible actions, consume approval inside the same idempotency boundary that starts execution. Otherwise two workers can race to reuse one approval.
The contract also makes safer API design observable. Gmail exposes trash and untrash separately from delete, which immediately and permanently deletes a message. An email agent intended for cleanup should not receive the permanent-delete tool at all. If long-term deletion is required, a separate retention service can empty Trash under a delayed policy after backups, holds, and notices are satisfied.
Every compensation needs its own authorization and evidence. A refund, rollback deploy, reopened ticket, or untrash call is another state change, not a magical exception. Bind it to the original action receipt, record why it started, and verify the result against the target system.
Separate proposal, commit, recovery, and reconciliation
Proposal planeThe model selects a registered tool and arguments. The runtime normalizes targets and resolves the reviewed reversibility contract.
Decision planePolicy checks identity, state, risk class, blast radius, approval digest, expiry, and point-of-no-return rules.
Commit planeAn executor sends one idempotent request and stores receiver evidence before reporting success to the agent.
Recovery planeA durable saga service runs inverse or compensating actions without asking the model to remember what to undo.
Reconciliation planeAn independent query checks world state, downstream copies, residual effects, and whether manual repair is still required.
The recovery service must outlive the chat turn. Put saga state in durable storage with explicit transitions such as proposed, approved, executing, committed, compensating, compensated, reconciled, and manual_repair. A model process crash cannot erase a pending compensation.
Keep a control channel independent from the agent's conversational queue. A stop request should revoke new permits and signal active workers. It cannot assume a third-party API call can be cancelled after commit. The executor records whether cancellation occurred before dispatch, during an abortable request, after an unknown timeout, or after confirmed commit. Each state leads to a different recovery action.
Use receiver-side evidence where consequences matter. An agent log saying “message trashed” proves only what the agent reported. Gmail's message ID, label state, and history identifier provide stronger confirmation. For a payment, use provider transaction state and ledger reconciliation. For a deploy, use the orchestrator revision and health checks. Recovery ends when world state is verified, not when the compensation function returns 200.
Worked example: make inbox cleanup fail safely
Suppose an agent must review 4,000 messages and propose cleanup. The naive design streams message content into a long conversation, keeps “confirm before deleting” in the prompt, and gives the agent a bulk delete tool. The larger real mailbox can change context behavior, a stale instruction can disappear from the model's active view, and one tool call can create a large blast radius.
The safer design separates classification from execution. The agent writes a proposed set containing message IDs, reasons, confidence, and immutable message versions. A deterministic service checks exclusions such as legal holds, starred mail, protected senders, recent messages, and retention rules. The user approves an exact digest. A worker moves at most ten messages per batch to Trash, captures the new label and history state, and stops between batches when the approval expires or policy changes.
If the worker receives a timeout, it does not repeat the trash request blindly. It queries each message. Items already in Trash are recorded as committed. Items still in the inbox remain pending. Unknown items move to reconciliation. If the user withdraws approval, new batches stop and already trashed messages are untrashed from the receiver records. If untrash fails for three messages, the saga enters manual_repair, revokes mailbox write access, and creates an operator packet with exact IDs and failure responses.
| Naive control | Failure | Reversibility control |
| “Confirm first” in prompt | Can be lost, reinterpreted, or bypassed | Action-bound permit outside model context |
| Bulk permanent delete | No reliable inverse | Trash only, small batches, delayed retention deletion |
| Retry on timeout | Duplicate or expanded side effect | Receiver query plus idempotency and reconciliation |
| Stop chat message | May arrive after commit | Independent permit revocation and worker cancellation |
| Agent says “restored” | Self-reported, incomplete outcome | Receiver state check and exception register |
This architecture does not claim zero risk. Messages in Trash may have triggered rules, notifications, or downstream synchronization. The reconciliation query should include those known consumers where possible. If the original effect crossed a boundary the system cannot inspect, the receipt must say so.
Run compensation as deterministic workflow code
Do not ask the model to improvise the inverse sequence. The model can suggest a repair plan, but reviewed orchestration code owns execution. The simplified TypeScript below shows the control shape.
async function executeWithRecovery(action: ProposedAction) {
const contract = registry.resolve(action.tool, action.target);
const snapshot = await contract.readState(action.target);
const digest = hash({ action, snapshot, contract: contract.version });
const permit = await policy.consumePermit({
digest,
idempotencyKey: action.idempotencyKey,
expiresAt: action.approvalExpiresAt
});
if (!permit.allowed) throw new Denied(permit.reason);
const saga = await sagas.begin({ digest, contract, snapshot });
try {
const result = await contract.execute(action, permit);
await saga.recordCommit(result.receiverReceipt);
const observed = await contract.reconcile(action.target);
if (!contract.accepts(observed)) throw new OutcomeMismatch(observed);
return await saga.markReconciled(observed);
} catch (error) {
const commitState = await contract.detectCommit(action.target, saga);
if (commitState === "not_committed") return saga.markAborted(error);
if (contract.reversibility.class === "irreversible") {
await access.revoke(action.principal);
return saga.escalate("irreversible_side_effect", error);
}
const repair = await contract.compensate(saga.originalReceipt);
const repairedState = await contract.reconcile(action.target);
if (!contract.compensationAccepts(repairedState, repair)) {
await access.revoke(action.principal);
return saga.escalate("compensation_incomplete", { error, repair });
}
return saga.markCompensated(repairedState);
}
}
Real workflows often have several committed steps. The compensating transaction pattern warns that repair does not always run in literal reverse order. Undo the most sensitive inconsistency first. A payment workflow might release an inventory reservation before sending a customer correction, or restore account access before fixing an analytics copy. Encode dependency and risk priority in the saga definition.
Compensation must be idempotent too. The orchestrator can crash after the inverse commits but before it records success. Give each repair an idempotency key derived from the original receipt and compensation version. On retry, query receiver state and continue from proven facts.
Failure modes a rollback demo usually misses
| Failure mode | What goes wrong | Required defense |
| Hidden permanent path | Tool wrapper exposes delete behind a generic “clean up” operation | Capability inventory, distinct tool IDs, deny permanent path by default |
| Approval drift | Resource or policy changes after approval | Bind version and state digest; expire and re-request |
| Permit replay | Two workers consume one approval | One-time permit consumed atomically with idempotency boundary |
| Unknown commit | Timeout occurs after receiver changes state | Query receiver before retry or compensation |
| Partial compensation | Some resources restore while others fail | Per-item receipts, exception register, access revocation, named repair owner |
| False rollback | Correction is described as erasing a sent or public event | Record residual effect and use honest “compensated” status |
| Context-dependent invariant | Safety rule disappears after compaction | Host policy and reviewed tool contract outside model context |
| Stop-path illusion | Chat stop cannot reach queued or remote work | Independent cancellation, permit revocation, worker heartbeat |
| Self-attested recovery | Agent declares success without checking world state | Receiver evidence and independent reconciliation query |
| Ownerless repair | Manual step exists but nobody is paged or authorized | On-call owner, runbook, access, deadline, closure evidence |
Start with ten tools and run a failure drill
Inventory the ten tools with the highest combination of impact, frequency, blast radius, and weak recovery. For each, list the target system, operation, principal, reversible class, point of no return, inverse or compensation, undo window, downstream consumers, receiver evidence, reconciliation query, and manual owner. Remove any operation whose contract cannot be completed.
Prefer staging primitivesDraft before send, Trash before permanent delete, preview before publish, reserve before charge, and canary before broad deploy.
Bind exact approvalHash normalized tool, arguments, target version, policy, scope, expiry, and idempotency key.
Limit each commitCap batch size, rate, value, recipients, targets, and total side effects under one permit.
Store receiver evidenceCapture provider identifiers, resource versions, result state, and a controlled reference to the original request.
Make compensation durableRun recovery from a persisted saga service that survives model, worker, and chat failures.
Verify the repaired worldCheck target and known downstream state; record residual effects and unknowns.
Revoke on uncertaintyStop new writes when commit or compensation state cannot be proven.
Name manual ownershipGive the repair team a pager, permissions, evidence packet, deadline, and closure gate.
Then run a game day. Inject a timeout after the receiver commits. Duplicate a queue delivery. Expire approval between two batch items. Change resource version after approval. Make the third compensation fail. Drop the model's original instruction through context compaction. Send stop while two workers are active. Verify that the system contains blast radius, records uncertainty, revokes permits, produces a repair packet, and refuses to mark the workflow restored until reconciliation passes.
Measure median time to detect unknown state, percentage of writes with complete reversibility contracts, compensation success by class, unreconciled side effects, manual repair time, repeat side effects prevented by idempotency, and actions removed because no safe recovery existed. A lower autonomy rate after the inventory can be a success: the system learned which one-way doors should remain human-controlled.
Frequently asked questions
What is an AI agent reversibility contract?
It is a runtime-enforced description of how one state-changing tool action can be undone, compensated, reconciled, or escalated. It includes class, preconditions, approval binding, idempotency, undo windows, evidence, receiver checks, and repair ownership.
Is a compensating transaction the same as rollback?
No. Database rollback prevents or removes changes inside a transaction boundary. Compensation happens after one or more effects commit. It creates new actions that offset or repair the outcome, and the original event may remain visible.
Does human approval solve irreversible actions?
It helps only when bound to the exact current action. Approval does not make a public message unread, a secret undisclosed, or a cleared transfer reversible. Use staging, small scope, stronger validation, and a final point-of-no-return gate.
Should the model choose the compensation?
The model may propose or explain a repair, but reviewed runtime code should select and execute registered compensation. The target system's verified state, not the model's confidence, decides whether the repair succeeded.
What is the first production change to make?
Remove permanent and bulk-destructive APIs from general agent toolsets. Replace them with staging or soft-delete operations, small batch limits, exact approvals, durable receipts, and a tested manual repair path.
Sources and further reading
Current documentation and recent community pages were checked on September 2, 2026. The Summer Yue event is a reported February incident resurfaced in late-August discussion, not a newly reproduced test.