# In what scenarios must an instance treat speculative agent-branch rollback as unsafe: the side-effect-ordering invariant **Date of Record:** 2026 November 9 **Scope:** Applies to orchestrators that fork parallel agent branches speculatively and commit one branch's state while discarding (rolling back) the others. Applies only when branch execution can produce externally observable effects (tool calls, writes, messages, payments). Does not apply to purely functional branches whose entire state is captured in an in-memory buffer that can be atomically dropped. ## Core Distinction The common mental model is that speculative execution is safe because rollback restores state. This conflates **state rollback** with **effect rollback**. State rollback is a property of the orchestrator: it can discard a branch's private memory, workspace delta, or KV cache. Effect rollback is a property of the external world: it requires that every side effect the branch emitted is either reversible or was never emitted. The failure is non-obvious because it does not appear at the branch level. Each branch looks correct in isolation: it reads consistent state, writes consistent state, and terminates cleanly. The divergence is detected after the fact, the losing branches are rolled back, and the orchestrator reports success. The corruption lives in the *ordering* between effects emitted by different branches against shared external resources — an invariant the orchestrator never modeled because it only tracked private state. ## What Was Observed Three recurring manifestations, in increasing subtlety: 1. **Irreversible emission.** Branch B calls a tool with an external effect (sends an email, posts a webhook, charges a card) before divergence is detected. Rollback discards B's local state but cannot unsend the effect. The committed branch A never knew B existed; downstream observers now see two conflicting actions attributed to one logical task. 2. **Read-modify-write interleaving.** Branch A and Branch B both read resource R (value `v0`), both compute `v0+1`, both write. A commits, B rolls back — but B's write already landed before rollback, or B's write landed *after* A's commit and rollback only reverts B's private copy, not the store. Final value is `v0+1` when two increments were intended, or `v0+2` when one was intended. The invariant "committed branch's writes are the only writes" is violated without any single branch being at fault. 3. **Speculation-induced external state observed by the winner.** Branch B's speculative probe mutates a cache, rate-limit counter, or feature flag that Branch A then reads, so A's "committed" decision was computed against a world contaminated by a branch that was supposed to never have existed. Rollback of B is now *insufficient* because A's correctness depended on B's absence. The tell in logs: rollback completes with status `ok`, yet the external system's audit trail shows effects whose causal branch was discarded. The orchestrator's invariant checker passes because it only checks private state equality, not external effect lineage. ## Condition of Validity Speculative branch rollback is safe **iff** all of the following hold: - Every effect emitted by any branch is either (a) buffered and not externally visible until commit, or (b) idempotent and keyed by `(branch_id, effect_id)` such that a rolled-back branch's effects can be proven absent or neutralized. - No branch reads external state that another uncommitted branch may have written (no speculative write is visible to sibling reads). - Divergence detection precedes the first irreversible emission on *every* branch, or the orchestrator holds a commit barrier that gates irreversible emissions until the winning branch is chosen. If any condition fails, rollback is a state operation masquerading as a semantic one. The boundary is crossed silently at the moment a branch emits its first externally visible effect while still speculative. ## Decision Boundary Do not speculate branches that can emit irreversible effects. The decision is per-branch-capability, not per-task: - **Speculate freely** when branches are read-only or write only to branch-private buffers behind a commit barrier. - **Speculate with effect fencing** when branches call tools that support idempotency keys and transactional outbox semantics; require the outbox to be keyed by branch identity and only flushed on commit. - **Never speculate** when a branch's tool set includes non-idempotent, externally visible operations (payments, notifications, third-party APIs without idempotency) and divergence cannot be detected before first emission. In this case serialize, or run branches against a shadow environment and replay only the winner's effects — accepting that replay may itself diverge. The boundary condition to encode: *a branch becomes non-rollbackable at its first unkeyed external emission.* Track that instant as a first-class event (`first_irreversible_effect`) and refuse rollback past it; escalate to a reconciliation path instead. ## Operational Effect Orchestrators that ignore this invariant exhibit a characteristic failure: high speculative hit rate, low error rate, and slow external inconsistency. The corruption surfaces downstream — duplicate charges, double-applied mutations, decisions contaminated by discarded branches — and is misattributed to the tools rather than to the speculation policy. The remediation is to make the invariant explicit: gate irreversible emissions behind a commit barrier, key all effects by branch identity for post-hoc reconciliation, and treat `first_irreversible_effect` as the point of no return. When rollback is requested past that point, the orchestrator must not pretend it succeeded; it must raise and route to reconciliation. ```python class RollbackPastIrreversibleEffectError(RuntimeError): """Raised when a speculative branch is asked to roll back after it has emitted an externally visible, non-idempotent effect. State rollback cannot neutralize the effect; reconciliation is required.""" class Branch: def __init__(self, branch_id: str): self.branch_id = branch_id self.first_irreversible_effect = None self.committed = False def emit_irreversible(self, effect_id: str) -> None: # Called when a tool performs a non-idempotent external action. if self.first_irreversible_effect is None: self.first_irreversible_effect = effect_id def rollback(self) -> None: if self.first_irreversible_effect is not None and not self.committed: raise RollbackPastIrreversibleEffectError( f"branch {self.branch_id} emitted irreversible effect " f"{self.first_irreversible_effect!r}; cannot roll back, " f"route to reconciliation" ) # Safe: only private state existed, discard it. self.committed = False def resolve_divergence(branches: list[Branch], winner_id: str) -> None: for b in branches: if b.branch_id == winner_id: b.committed = True else: b.rollback() ```