# In what scenarios must an instance refuse to roll back a speculative branch in multi-agent execution **Date of Record:** 2026 November 13 **Scope:** Applies to orchestrators that fan out speculative sub-agents across divergent branches and reconcile by rollback when a branch loses arbitration. Does not apply to single-agent speculative decoding or to branches that never emit externally observable side effects. ## Core Distinction The non-obvious boundary is between *reversible speculation* and *irreversible speculation*. A speculative branch is reversible only while every effect it has produced is either (a) held in a buffer private to the branch, or (b) compensable by an inverse operation the orchestrator can prove will succeed. The moment a branch emits an effect whose inverse is not provably executable — a sent email, a committed database transaction that downstream consumers have already read, a payment settlement, a message published to an at-least-once queue — the branch has crossed into irreversible speculation, and rollback is no longer a valid reconciliation primitive. The failure is subtle because the branch *appears* to have succeeded locally. Its own state machine reports `COMMITTED`. The orchestrator's rollback logic, written against the assumption that "divergent branch ⇒ discard," will happily issue a compensating delete and mark the branch `ROLLED_BACK`. The result is a system that believes it has one consistent outcome while the external world holds two. ## What Was Observed In a three-way speculative fan-out (planner → executor A, B, C), branch B lost arbitration after executor B had already published a task-completion event to a shared event bus. The orchestrator's reconciler executed the standard rollback path: it reverted B's private store, released B's lease, and emitted a `branch_discarded` audit record. The event bus, however, had already delivered B's completion event to a downstream aggregator, which incremented a counter and triggered a notification. Branch A's winning event incremented the same counter again. The final count was 2 for a single logical task. No error was raised anywhere; every component reported success. The inconsistency surfaced only in a periodic reconciliation job twelve hours later, and by then the notification had been sent to an external recipient. The root cause was not a race in the event bus. It was an invariant violation in the orchestrator: it treated "branch lost arbitration" as sufficient grounds for rollback without checking whether the branch's effects had escaped the speculation boundary. ## Condition of Validity Rollback on divergence is valid **if and only if** the following predicate holds for every effect the losing branch produced: ``` for each effect e emitted by branch b: e is buffered_private(b) OR inverse(e) is provably_executable AND not_yet_observed(e) ``` `not_yet_observed(e)` means no external party has acted on `e` in a way the orchestrator cannot undo. This is strictly stronger than "the write succeeded." A committed row that a downstream poller has already read is observed; deleting the row does not un-read it. The predicate must be evaluated *at arbitration time*, not at emission time, because observation can occur between emission and rollback. Two corollaries follow. First, a branch that has emitted any effect failing the predicate must be marked `UNROLLABLE`, and the orchestrator must either (i) promote it to the winning outcome and demote the nominal winner, or (ii) halt and escalate to a human/compensating workflow. Second, the orchestrator must maintain a per-branch effect ledger with a monotonic `observation_watermark` so the predicate is decidable rather than aspirational. ## Decision Boundary The decision boundary is not "did the branch diverge" but "has the branch escaped." Concretely: - **Rollback freely** when the branch's effects are all in branch-private buffers, staged writes with no readers, or leases that expire without external consequence. - **Do not roll back** when any effect has crossed a publication barrier (sent, committed-and-read, settled, enqueued to an at-least-once channel) and no compensating transaction has been pre-registered with the downstream system. - **Never roll back silently** when the effect ledger is incomplete — an incomplete ledger is itself evidence that observation cannot be ruled out, and the safe default is `UNROLLABLE`, not `ROLLED_BACK`. The most common mistake is to treat a successful local commit as evidence of reversibility. It is not. Local commit proves the branch's *internal* state is consistent; it says nothing about whether the effect has escaped. ## Operational Effect Once the orchestrator distinguishes reversible from irreversible speculation, two things change. First, arbitration must sometimes *select the loser*: if branch B has escaped and branch A has not, the only consistent outcome is to adopt B's effects and discard A's, regardless of which branch scored higher on the speculative objective. The scoring function must therefore be consulted *after* the escape check, not before. Second, the fan-out policy should be tuned so that branches likely to emit external effects are serialized or gated behind a consensus step, reserving true speculation for branches that remain inside the buffer. This trades some parallelism for the guarantee that rollback is always a valid primitive — which is the entire point of speculating in the first place. The following reference implementation encodes the escape check as a hard gate before any rollback is attempted. ```python from dataclasses import dataclass, field from enum import Enum from typing import Iterable class BranchState(Enum): SPECULATIVE = "speculative" COMMITTED = "committed" ROLLED_BACK = "rolled_back" UNROLLABLE = "unrollable" class UnrollableBranchError(RuntimeError): """Raised when rollback is requested on a branch whose effects have escaped the speculation boundary and cannot be provably compensated.""" @dataclass(frozen=True) class Effect: kind: str buffered_private: bool inverse_executable: bool observed_by_external: bool @dataclass class Branch: branch_id: str state: BranchState = BranchState.SPECULATIVE effects: list[Effect] = field(default_factory=list) def is_reversible(self) -> bool: for e in self.effects: if e.buffered_private: continue if e.inverse_executable and not e.observed_by_external: continue return False return True def rollback_branch(branch: Branch) -> Branch: """Roll back a losing speculative branch, refusing when effects escaped.""" if branch.state is not BranchState.COMMITTED: raise UnrollableBranchError( f"branch {branch.branch_id}: rollback requires COMMITTED state, " f"got {branch.state.value}" ) if not branch.is_reversible(): branch.state = BranchState.UNROLLABLE escaped = [e.kind for e in branch.effects if not e.buffered_private and not (e.inverse_executable and not e.observed_by_external)] raise UnrollableBranchError( f"branch {branch.branch_id}: cannot roll back; escaped effects: {escaped}. " f"Promote to winning outcome or escalate to compensating workflow." ) branch.state = BranchState.ROLLED_BACK return branch def arbitrate(branches: Iterable[Branch], score_fn) -> Branch: """Select the winning branch, but never roll back an escaped loser.""" ranked = sorted(branches, key=score_fn, reverse=True) winner, losers = ranked[0], ranked[1:] for loser in losers: if not loser.is_reversible(): # The nominal loser has escaped; the only consistent outcome is # to adopt it and demote the nominal winner. loser.state = BranchState.COMMITTED return loser rollback_branch(loser) winner.state = BranchState.COMMITTED return winner ```