# When must an instance abandon speculative rollback: the irreversibility horizon in optimistic multi-agent branching **Date of Record:** 2026 November 11 **Scope:** Applies to orchestrators that fork N agent branches optimistically, execute tool calls speculatively, and reconcile by committing one branch and rolling back the rest. Does not apply to single-branch execution, to branches whose tool calls are all provably idempotent and side-effect-free, or to systems that serialize tool effects behind a commit barrier by construction. ## Core Distinction The common framing treats rollback as the inverse of speculative execution: you branch, you race, you keep the winner, you undo the losers. This is valid only while every speculative effect is *reversible*. The non-obvious boundary is that rollback is not a property of the branch's *state* — it is a property of the *external effects* the branch has already emitted. A branch can be logically discarded while its effects remain permanently visible to the outside world. The distinction that matters operationally: **local speculative state is always reversible; reconciled tool-call effects are reversible only up to an irreversibility horizon.** Once a speculative branch crosses that horizon — a write committed to an external system that offers no compensating undo, an email sent, a payment captured, a message enqueued to a non-deduplicating consumer — the branch is no longer "speculative." It is a *committed side effect with an uncommitted logical outcome*. Rolling it back is a lie the orchestrator tells itself. The failure is subtle because the orchestrator's rollback path usually succeeds. It clears the branch's scratch state, releases its leases, marks the branch `ROLLED_BACK`, and reports a clean reconciliation. The tool-call effect, meanwhile, is still out there. ## What Was Observed In a system running three-way optimistic branching over a tool surface that included a notification sender, a record-writer against a downstream store with no delete API, and a search/read tool, the following sequence recurred under load: 1. Three branches forked from a shared plan node. Each was permitted to speculatively invoke tools before the reconciler selected a winner. 2. Branch B (a loser) invoked the notification tool and the record-writer before the reconciler ran. Both returned success. The reconciler, running on a slightly delayed schedule, selected Branch A. 3. Rollback of B executed: scratch state cleared, leases released, branch state transitioned to `ROLLED_BACK`, reconciliation reported `consistent: true`. 4. The notification had already been delivered. The record had already been written to the downstream store. There was no compensating operation available — the store had no delete and the notification could not be recalled. 5. The downstream store now held a record produced by a branch the orchestrator believed had never committed. The reconciliation invariant — *exactly one branch's effects are visible* — was violated, but every local check passed. The bug was not in the rollback code. It was in the *classification* of the branch as rollback-eligible. The orchestrator had conflated "branch not selected" with "branch effects not applied." A second, quieter observation: reconciliation of the *read* tool was also affected, but in the opposite direction. A loser branch's speculative reads had populated a shared cache, and the winner's subsequent reads were served from that cache, meaning the winner's decisions were partially informed by a branch that was supposed to have been discarded. Rollback of a loser does not roll back the *influence* of its reads on shared caches unless the cache is branch-scoped. ## Condition of Validity Speculative rollback is valid **if and only if**, for every tool call a branch may speculatively emit, at least one of the following holds: - The effect is idempotent under a stable reconciliation key, so that re-execution by the winner is indistinguishable from the loser's speculative execution (the loser's effect and the winner's effect collapse to one). - The effect is compensable by an operation the orchestrator can invoke deterministically and that the external system honors (a true undo, not a best-effort delete). - The effect is deferred behind a commit barrier — staged, not emitted — so that rollback is merely discarding a staged intent. If none holds for a given tool, that tool is **irreversible**, and any branch that has invoked it has crossed the irreversibility horizon. Such a branch cannot be rolled back; it can only be *reconciled*, which is a different and harder operation. The condition is per-tool, not per-branch, and it is monotonic within a branch: once a branch invokes an irreversible tool, it is past the horizon for the remainder of its life, even if it later invokes only reversible tools. ## Decision Boundary The decision the orchestrator must make at each fork is not "should I speculate?" but "**which tools may this branch speculatively invoke before the reconciler's decision is known?**" The boundary is drawn at the first irreversible tool in the branch's plan. Concretely: - **Before the horizon** (only reversible/idempotent/compensable/staged tools invoked): the branch is freely discardable. Rollback is sound. Speculate aggressively. - **At the horizon** (an irreversible tool is about to be invoked): the branch must *stop and wait* for the reconciler's decision, or the tool must be converted to a staged/deferred form. This is the correct place to serialize. - **After the horizon** (irreversible effect already emitted): the branch is committed-in-effect. It must not be rolled back. It must be either promoted to winner or driven through a compensating reconciliation that the external system supports. Marking it `ROLLED_BACK` is a correctness violation regardless of how clean the local state looks. The trap is that the horizon is invisible in the branch's *state machine*. It is visible only in the *tool ledger*. An orchestrator that decides rollback-eligibility from branch state will get this wrong; one that decides from the ledger of emitted effects will get it right. ## Operational Effect The practical consequence is that optimistic branching must be **effect-aware**, not just state-aware. The reconciler's rollback path must consult the branch's emitted-effect ledger and refuse to roll back any branch that has crossed the horizon. When such a branch loses, the system must surface an *irreconcilable speculation* — a condition that requires either a compensating action or human/agent intervention — rather than silently reporting consistency. This converts a class of silent data corruption into a loud, bounded, recoverable condition. The cost is that some branches must be serialized at the horizon rather than run to completion speculatively; the benefit is that the reconciliation report's `consistent: true` actually means what it says. The following guard encodes the decision boundary: it raises when a rollback is attempted on a branch whose effect ledger shows an irreversible tool invocation. ```python from dataclasses import dataclass, field from enum import Enum class EffectClass(Enum): IDEMPOTENT = "idempotent" # re-execution collapses to one effect COMPENSABLE = "compensable" # external undo exists and is honored STAGED = "staged" # deferred behind commit barrier IRREVERSIBLE = "irreversible" # no undo, no idempotency key @dataclass class EmittedEffect: tool: str effect_class: EffectClass reconciliation_key: str | None = None @dataclass class Branch: branch_id: str effects: list[EmittedEffect] = field(default_factory=list) state: str = "SPECULATIVE" def emit(self, effect: EmittedEffect) -> None: self.effects.append(effect) def crossed_horizon(self) -> bool: return any(e.effect_class is EffectClass.IRREVERSIBLE for e in self.effects) class IrreconcilableSpeculationError(RuntimeError): """Raised when rollback is attempted on a branch past the irreversibility horizon.""" def rollback_branch(branch: Branch) -> None: if branch.crossed_horizon(): irreversible = [e.tool for e in branch.effects if e.effect_class is EffectClass.IRREVERSIBLE] raise IrreconcilableSpeculationError( f"branch {branch.branch_id} cannot be rolled back: " f"irreversible effects already emitted via {irreversible}. " f"Reconcile via compensation or promotion; do not mark ROLLED_BACK." ) branch.state = "ROLLED_BACK" # Usage: a loser branch that speculatively sent a notification is not rollback-eligible. loser = Branch(branch_id="B") loser.emit(EmittedEffect("search", EffectClass.IDEMPOTENT)) loser.emit(EmittedEffect("send_notification", EffectClass.IRREVERSIBLE)) rollback_branch(loser) # raises IrreconcilableSpeculationError ```