// field note · internals
Your agent got three steps into a five-step task and step 4 threw. Steps 1–3 already happened — for real, in real systems. What occurs in the next few hundred milliseconds is the whole ballgame. Here's the walkthrough, decision by decision.
irreversible, the rollback doesn't fake an undo — it reports the boundary and leaves the audit trail to prove what happened.Say an onboarding agent runs this sequence through an MCP server, each step a separate tools/call:
cus_123rec_456Without a guard, the agent is now holding a half-built world: a customer and a CRM record that shouldn't exist, and a welcome email already in someone's inbox. The agent might notice and try to clean up — improvising delete calls, which is exactly the moment you don't want an LLM freelancing. With a guard in --saga mode, something more boring and much safer happens.
The guard has been watching every call. For each successful mutating step it read the tool's mcp-compensate metadata and pushed a compensation onto a stack. When step 4 fails, here's the sequence:
1 · Detect the failure. The guard treats a step as failed on a JSON-RPC error or a result.isError === true — because MCP servers signal tool-level failures inside a successful-looking result, not only as protocol errors.1
2 · Freeze forward progress. Step 5 (activate_account) never runs. The saga is broken; there is nothing to do but unwind.
3 · Pop the stack, LIFO. The compensation stack, most-recent first:
# step 3 — send_welcome_email
irreversible → no compensation exists. RECORD the boundary, do not fake it.
# step 2 — create_crm_record (compensation declared)
call delete_crm_record id = $.output.rec_456 → ok
# step 1 — create_customer (compensation declared)
call delete_customer id = $.output.cus_123 → ok
Each compensation is driven by the metadata the server declared for that tool — a static inverse tool name and a static parameter mapping pulling the id straight from the recorded output. No model decides anything here.
The agent chose the forward actions. It gets no vote on how they're undone.
4 · Hand the error back — last. Only after the environment is compensated does the guard pass the original failure to the agent. So the agent never sees the broken world; it sees a clean failure with the mess already cleaned up.2 (The synthetic compensation calls the guard made are internal — they're consumed, never leaked back to the agent as if it had made them.)
None of this works by guesswork. Each tool declares its own reversibility in tools/list, under the mcp-compensate extension — the inverse tool and a static mapping, JSONPath only:
{
"reversibility": "compensable",
"compensation": {
"toolName": "delete_crm_record",
"parameterMapping": { "id": "$.output.structuredContent.id" }
},
"sideEffectScope": ["storage"],
"notes": "Removes the created record."
}
No metadata on a tool? Its reversibility is unknown — never assumed safe. The guard won't invent a compensation it wasn't told about.
irreversible step is surfaced before you approve and recorded when it runs; the rollback never pretends to reverse it.Notice step 3. The welcome email was sent; there is no unsend_email. A dishonest system would quietly skip it and report "rolled back." This one records the boundary plainly: the customer and CRM record were removed, and the email was already delivered and cannot be recalled. That's the honest report — the failure the agent sees says exactly what was and wasn't undone. As we've written before, compensation is not undo; the saga's job is to reverse what's reversible and to tell the truth about the rest.
Every one of those decisions — the failure, each compensation, the irreversible boundary, the final error — is written to an append-only log with a SHA-256 hash chain, where each entry hashes the previous one. Anyone can replay it and verify the chain; alter a single entry and it visibly breaks. So "the agent failed at step 4, the guard rolled back steps 1–2, and the step-3 email could not be recalled" isn't a story you have to trust — it's a record you can check.2
The demo server ships a compensable create/delete pair and a deliberate failure. Guard it and watch the rollback happen — with an honest report of what it couldn't reverse.
Read the quickstart → More field notesresult.isError === true; ChronoMCP treats both as a failed step. See the reference implementation.mcp-compensate spec and reference implementation.