A sales agent is tidying up the Q4 pipeline. It marks deal D-881 as Won in the CRM, books a kickoff meeting with the customer, and emails their buyer a welcome note. An hour later you notice it picked the wrong deal. The customer hasn't signed.
To clean up, you open three systems, work out what changed, put back the deal stage (unless a colleague has edited it since), cancel the meeting (which tells the attendees), and accept that the email has gone.
In my last post I described HATP, a draft protocol that makes multi-step agent actions all-or-nothing before they happen: plan, prepare, get a human's signed approval, then commit. Your comments on that post shaped most of v0.2, so thank you.
But most agents don't plan. They call one tool at a time, and you find out afterwards. So for v0.3 I've drafted the other half: the Journal, undo for agents. Nothing is built yet. I'd like your opinion before we start.
The gap
| What exists | What it does | What's missing |
|---|---|---|
MCP tool annotations (destructiveHint, idempotentHint) |
Tell the client a tool might be dangerous | Hints only: no way to reverse anything |
| Sagas and compensating transactions | Undo steps inside one application | Nothing that works across independent tool servers an agent calls |
HATP v0.1 compensate tier |
Undoes steps when a transaction fails partway | Only inside a planned transaction, and only before it succeeds |
| Agent traces and logs | Show what happened | Can't take anything back |
Idea 1: every effect comes back with its undo
A Recorder sits between the agent and its tool servers, usually an MCP gateway. The agent needs no changes. When a tool server supports the journal, its normal tool result carries three extra things: what changed, a token to reverse it, and how long reversal stays possible.
"_meta": { "hatp/journal": {
"undo": { "kind": "compensate", "undoBy": "2026-11-07T14:02:10Z" },
"compensationToken": "ctk_9QwX…",
"effect": { "summary": "Deal D-881 stage: Proposal → Won",
"resource": "crm://acme/deal/D-881", "version": "v13" },
"receipt": { "…": "signed by the CRM" } } }
The tool server writes the summary from the actual change, not from the agent's text, because a human reads it later when deciding what to undo. The Recorder chains every entry into a signed, hash-chained Run Record.
Tool servers that know nothing about HATP still get journaled. Their calls are recorded as unknown and treated as irreversible. So it works with any MCP server on day one, and gets better as servers add undo.
Each operation declares up front whether it can be undone:
| Operation | Declares | Why |
|---|---|---|
update_deal |
compensate, 7 days |
Old stage can be restored |
create_event |
compensate, 30 days, side effect: "attendees get a cancellation notice" |
Undo is possible but visible |
refund_order |
compensate, 1 day, cost $2.50 |
Undo has a fee |
send_email |
none: "delivered to external recipients" |
Can't unsend |
export_report |
none: "discloses data outside the organisation" |
A read you can't take back |
Idea 2: autonomy is granted by reversibility
Once every operation says whether it can be undone, you can stop asking "should this agent be allowed to act?" and ask "can we take it back if it's wrong?"
{ "autonomy": { "compensate": "allow", "unknown": "ask", "none": "ask",
"approver": "human://acme.com/role/team-lead" } }
The agent acts freely on anything reversible. Before anything irreversible, the Recorder doesn't forward the call: it runs it as a normal HATP transaction with a human approval, so the email goes out only after someone signs off. Your organisation sets this policy, not the agent, and the agent can't change it.
Undo is a decision, not a button
Reversing work can destroy someone's newer edits or notify people outside the company, so a reversal goes through approval like any other risky action.
You ask to undo the run "from step 2 onwards". The Recorder then:
- adds dependent steps: if step 5 used step 2's result, undoing 2 alone would leave 5 pointing at nothing;
- checks each step: asks each tool server, without changing anything, whether undo would work right now;
- builds a Reversal Plan in reverse order, listing what can't be undone, and routes it to a human through the Human Gateway:
Undo 2 actions by sales-bot for Dana:
1. Cancel "Kickoff with Globex" on Nov 4 (attendees get a cancellation notice)
2. Deal D-881 stage: Won → Proposal
Cannot be undone:
- Email to buyer@globex.com (delivered to external recipients)
- runs the compensations in reverse order after approval, and records the outcome. If one fails, it doesn't undo anything that the failed step still depends on, and it reports exactly what was reversed, failed, or skipped.
For what can't be undone, it suggests a remedy, such as "send a correction email", but never performs it. A corrective action is a new action and needs fresh consent.
Never overwrite newer human work
Between the agent's change and your undo, Eve may have moved the deal to "Negotiation". Blindly restoring "Proposal" would wipe out her change.
So every compensation carries ifVersion: the version the agent left behind (v13). If the record has changed since, the tool server refuses with UNDO_CONFLICT and says what changed. The approval screen then shows the conflict, and only a human can choose to overwrite it.
Undo after a transaction commits
HATP v0.1 compensates only when a transaction fails partway. With the Journal, a coordinator records a committed transaction as a run, so the same tokens can reverse it later: "that trip committed fine, but it was the wrong week."
What it doesn't do
- Undo reads. A disclosure can't be taken back, so reads that send data outside the organisation are declared irreversible and fall under the autonomy policy.
- Redo. Fixing an undo is a new run.
- Prove the tool server really reversed the effect. Like everything in HATP, signatures prove who claimed what, not that the claim is true.
Status
- Spec draft: spec/hatp-journal.md. It builds only on v0.1 and lists every change it would make to it.
- No code yet. The work is planned in the v0.3 milestone: schema, a Recorder in the SDK, the MCP binding, and a demo where you undo part of an agent's run from the browser approver.
- v0.2 (pending votes, locks, Sessions) comes first.
I'd like feedback on
-
Would you return a
compensationTokenandundoByfrom your MCP server's tools? What would make that hard? -
Reversal skips
prepare. To keep undo cheap to adopt, tool servers get an optionalcheckinstead of the full two-phase flow; only the human votes. Is that too weak? - Dependencies. Agent hosts rarely know which earlier result a call used. Today the draft assumes "depends on everything earlier", which is safe but makes "undo just this step" pull in too much. Should the Recorder infer links from matching argument values?
- Reversibility-gated autonomy: is "act freely on what can be undone, ask before what can't" the right default, or too permissive?
- Long undo windows: if Dana changes roles before the window closes, should her authority be checked again at undo time?
Spec and SDK: https://github.com/probita-ai/hatp. Comments here or issues on GitHub are both welcome.
Top comments (6)
On question 4: how would you classify actions a computer-use tool performs by clicking through an app that has no API? There is no inverse call to return as a compensationToken, so would those always count as irreversible and need the human vote, or could a recorded undo path (the clicks that reverse it) qualify as compensation?
Asking from the Auten team: we build a computer-use MCP server and run into exactly this line.
Thanks, this is exactly the kind of case the draft needs to handle.
Short answer: a recorded undo path can count as compensation, but not just as raw reverse clicks, and by default it shouldn't get the same free pass as an API compensation.
The problem with replaying clicks is that the reverse of a UI action usually isn't the forward clicks backwards. "Undo create invoice" means finding the invoice and deleting it, not un-clicking "Save." The app's layout or state may also have changed by the time someone asks for the undo. An API compensation has ifVersion to catch that, and a click script has nothing like it.
So here's what I'd propose for v0.3:
Keep kind: "compensate", but add undo.via: "api" | "ui". The organisation's autonomy policy can then treat them differently. The default would be that via: "ui" compensations still need the human vote before the forward action runs. An org that trusts your server can relax that for specific apps.
compensationToken is an opaque handle to the undo procedure your server stored, not the clicks themselves. Your server owns how it's replayed.
A UI version of ifVersion: at action time, record a state fingerprint of the thing you changed (accessibility-tree snapshot, visible field values, record ID shown on screen). Before replaying, the server reads the current state. If it doesn't match, it returns UNDO_CONFLICT, the same as the API path.
Postcondition check after replay: confirm the UI now shows the pre-action state. If it doesn't, report the step as failed in the Reversal Plan outcome rather than reporting success.
undoBy follows the app's own window, e.g. how long its trash or recycle bin keeps items. If the app has no undo or delete for that thing at all, it's none, however clever the replay.
As a rule of thumb, an action qualifies as compensate via UI only if the app itself offers a way to reverse it (delete, restore from trash, revert) and your server can both check before and verify after. Everything else is none or unknown and goes through the vote.
Would you be willing to try returning via: "ui" plus a state fingerprint from your server? It would help to know what state you can reliably capture: accessibility tree, DOM, or only screenshots. I'd like to write this into the draft with a real implementation behind it.
Thanks, the via: "ui" split with a check before replay and a verify after makes sense to me, and so does the rule that the app itself has to offer the reverse.
What we can capture: the window's accessibility tree with its visible text, plus screenshots. One thing from practice: whole-tree snapshots shift between app versions and window sizes, so I'd fingerprint only the changed record (its on-screen ID and field values), not the tree.
Adding via: "ui" to our server isn't my call to make. I've passed your proposal to Andrius, who decides what goes in.
The main failure mode for compensation tokens in practice is state drift between the original action and the rollback attempt. If an agent updates a CRM record and a teammate edits the same record ten minutes later, restoring the old snapshot creates a silent lost update.
Unless the compensation token enforces optimistic concurrency, like checking a version tag or etag before applying the inverse, the tool server cannot safely do an automated rollback without domain-specific merge logic. In my own setups, making compensation strictly conditional on resource version has prevented far more accidents than trying to infer causal dependencies across tools.
Agreed, and thanks. That's now in the spec. Every undo carries ifVersion (an ETag or revision). If the record changed since the agent's action, the tool server returns UNDO_CONFLICT and changes nothing, and only a human can override it.
Your point also closed a gap: an action with no version can no longer be undone automatically. The agent has to ask before running it, and undoing it always needs a human:
github.com/probita-ai/hatp/pull/67.
I agree on priorities too: version checks do the real protecting, and inferring dependencies is only an optimisation.
On question 3 (dependencies): inferring links from matching argument values is a reasonable first pass, but it will miss cases where the dependency is implicit. A safer approach might be to let tool servers optionally declare their inputs, so the Recorder can build the graph from those declarations rather than guessing. Start with the heuristic, but design the schema so explicit declarations can be added later.