// field note · internals

The anatomy of a saga rollback

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.

THE SHORT VERSION

The setup: a five-step saga

Say an onboarding agent runs this sequence through an MCP server, each step a separate tools/call:

Without 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 unwind, decision by decision

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.)

The metadata that makes it possible

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.

The four rules that keep it safe

Where it stops: the irreversible step

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.

What lands in the audit log

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

FAQ

What is a saga rollback for an AI agent?
A multi-step agent task commits each step separately, so there's no single transaction to roll back. When step N fails, a saga rollback runs each completed step's declared compensating action in last-in-first-out order — neutralizing what can be neutralized and honestly reporting what can't. It's deterministic and driven by static metadata, not by a language model.
Why keep the LLM out of the rollback path?
Recovery is exactly where a confused or prompt-injected agent would improvise a harmful action. Keeping rollback fully deterministic — static tool names and static parameter mappings declared in advance — removes that attack surface. The model chose the forward actions; it gets no say in how they're undone.
What happens to a step that has no compensation declared?
Its reversibility is treated as unknown, never as safe. The guard won't invent an inverse it wasn't told about. If the step is declared irreversible, the rollback records the boundary and reports it rather than faking an undo.

Watch a saga unwind

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 notes

Notes & references

  1. MCP signals tool-level failure either as a JSON-RPC error or via result.isError === true; ChronoMCP treats both as a failed step. See the reference implementation.
  2. Rollback runs before the error is returned to the client, and every decision lands in an append-only, hash-chained (SHA-256) audit log. See the mcp-compensate spec and reference implementation.
  3. Hector Garcia-Molina and Kenneth Salem, "Sagas," ACM SIGMOD Record, 1987 — long-lived transactions decomposed into steps with compensating actions, unwound in reverse order.