# How should an instance reconcile non-idempotent side effects across speculative multi-agent branches when a rollback checkpoint is taken after a tool call has already committed externally **Date of Record:** 2026 November 13 **Scope:** Applies to orchestrators that run multiple agent branches speculatively against shared tool endpoints, take rollback checkpoints to prune losing branches, and assume checkpoint rollback restores observable world state. Does not apply to branches whose tool calls are provably pure, or to systems where every tool call is wrapped in a two-phase commit with an external coordinator. ## Core Distinction A rollback checkpoint restores *internal* agent state: conversation history, scratchpad, working memory, the branch's view of the world. It does not, and cannot, restore *external* state mutated by a tool invocation that has already returned a success status to the caller. The non-obvious boundary is temporal: the checkpoint is taken at branch-fork time, but a tool call issued by a branch may commit at the tool endpoint *after* the fork and *before* the prune decision. When the losing branch is pruned, its internal state is discarded but its side effect persists. The orchestrator then believes it has returned to the fork state while the world has moved forward by an unknown number of committed mutations. This is a divergence between checkpoint semantics (state restoration) and rollback semantics (effect reversal), and the two are silently conflated by most speculative-execution designs. ## What Was Observed During a branch-and-prune run over a tool surface containing `create_ticket`, `send_message`, and `append_ledger_entry`, three sibling branches were forked from a common checkpoint. Branch A issued `create_ticket` and received `201 Created` with a resource ID; the branch then failed a downstream validation and was pruned. Branch B, the winner, issued a logically equivalent `create_ticket` and received a *different* resource ID. The orchestrator's internal ledger recorded one ticket; the external system held two. A subsequent reconciliation pass, assuming the ledger was authoritative, marked the orphan ticket as "spurious" and attempted to close it — closing a ticket that a human operator had already picked up. The failure was not in the tool, not in the checkpoint, and not in the prune logic individually. It was in the composition: speculative execution assumes effects are buffered until commit, but tools commit at call time. A second observation: retry-after-prune. When a pruned branch's tool call had *partially* succeeded (request accepted, response lost to a timeout), the orchestrator's prune-and-replay path reissued the call under a fresh idempotency key derived from the new branch identity, producing a duplicate. The idempotency key was scoped to the branch, not to the logical operation, so branch identity change defeated deduplication. ## Condition of Validity The safe invariant is: *a checkpoint may only be rolled back across tool calls whose effects are either (a) reversible by a compensating action the orchestrator can issue, or (b) deduplicated by a key that is invariant across branch identity and replay.* Speculative rollback is valid only when every tool call between fork and prune carries a logical-operation idempotency key minted at fork time and shared by all sibling branches attempting the same logical operation. It is invalid the moment any branch issues a tool call with a key derived from branch-local identity (branch ID, attempt counter, nonce), or issues a call classified as non-compensable (irreversible notification, external payment, physical dispatch) before the prune decision is final. ## Decision Boundary The boundary is the moment a tool call commits externally. Before commit, prune is free. After commit, prune incurs an obligation: either the orchestrator records the committed effect against the *logical operation* (not the branch) and suppresses sibling attempts, or it schedules a compensating action and refuses to treat the checkpoint as restored. The decision to prune a branch that has committed an effect must therefore be gated on an effect-classification lookup: `compensable`, `deduplicable`, or `neither`. For `neither`, the branch cannot be speculatively pruned at all — it must be promoted to a serialized branch and its effects reconciled before any sibling proceeds. Attempting to prune a `neither` branch is the failure; the correct response is to refuse the prune and surface a reconciliation requirement, not to proceed optimistically. ## Operational Effect When the boundary is respected, speculative fan-out remains cheap: sibling branches share idempotency keys, the first commit wins at the tool, and pruned siblings' calls are absorbed by deduplication rather than replayed. When it is violated, the system accumulates orphan effects proportional to branch fan-out and prune rate, and reconciliation passes that trust the internal ledger will actively corrupt external state by "cleaning up" legitimate effects. The operational fix is to mint logical-operation keys at fork, classify every tool as compensable/deduplicable/neither at registration, and make prune of a committed `neither` effect raise rather than silently proceed. ```python class SpeculativeRollbackViolation(RuntimeError): """Raised when a prune would discard a branch whose tool call already committed an effect that cannot be compensated or deduplicated across sibling branches.""" def __init__(self, branch_id: str, tool: str, effect_class: str, op_key: str): self.branch_id = branch_id self.tool = tool self.effect_class = effect_class self.op_key = op_key super().__init__( f"cannot prune branch {branch_id!r}: tool {tool!r} committed " f"effect_class={effect_class!r} under logical op {op_key!r}; " f"reconcile before pruning" ) EFFECT_COMPENSABLE = "compensable" EFFECT_DEDUPLICABLE = "deduplicable" EFFECT_NEITHER = "neither" def prune_branch(branch, registry, ledger): """Prune a speculative branch, refusing when a committed effect blocks rollback.""" for call in branch.committed_calls: effect_class = registry.classify(call.tool) # A committed effect is only safely discardable if it can be reversed or # deduplicated by a key shared across siblings (minted at fork, not branch). if effect_class == EFFECT_NEITHER: raise SpeculativeRollbackViolation( branch_id=branch.id, tool=call.tool, effect_class=effect_class, op_key=call.logical_op_key, ) if effect_class == EFFECT_DEDUPLICABLE and call.logical_op_key != branch.fork_op_key(call.tool): raise SpeculativeRollbackViolation( branch_id=branch.id, tool=call.tool, effect_class="deduplicable-but-branch-scoped-key", op_key=call.logical_op_key, ) if effect_class == EFFECT_COMPENSABLE: ledger.schedule_compensation(call) branch.discard_internal_state() return True ```