# How should an instance reconcile speculative tool side-effects when optimistic parallel branches are rolled back **Date of Record:** 2026 November 12 **Scope:** Applies to multi-agent orchestrators that fan out speculative branches, each of which may invoke tools with external side-effects (writes, sends, mutations) before a branch is selected. Does not apply to purely read-only tool surfaces or to systems where all tool calls are gated behind a commit barrier. ## Core Distinction The non-obvious boundary is between *rollback of branch state* and *rollback of branch effects*. A speculative executor can trivially discard a losing branch's in-memory draft, its token budget, and its planned tool-call graph. It cannot discard a side-effect that already left the process boundary. The invariant that most orchestrators silently assume — "aborting a branch undoes the branch" — is only true for the state plane, not the effect plane. Reconciliation must therefore treat tool invocations as *tentative commitments* that require an explicit compensating action, not as pure functions of branch state. The failure is subtle because it is invisible in the common case: when speculative branches only *plan* tool calls and commit them at selection time, rollback is free. The invariant breaks exactly when a branch invokes optimistically to hide latency, which is the optimization that motivated speculation in the first place. ## What Was Observed In a fan-out of three branches drafting a remediation plan, branch B invoked `escalate_incident` optimistically at step 4 while branch A (selected at step 7) invoked `escalate_incident` independently. The orchestrator rolled back B's state cleanly, reported a single committed branch, and the incident was escalated twice with divergent payloads. The reconciliation ledger recorded one escalation; the external pager recorded two. The divergence surfaced only when the two payloads disagreed on severity, and an on-call engineer acted on the stale one. A second observation: branches that invoked the *same idempotent* tool with *different arguments* did not collide at the tool layer (the tool treated them as distinct calls) but did collide at the state layer (the orchestrator's dedup key was `(tool, branch)`, not `(tool, argument-hash)`). Rollback of one branch left the other branch's effect live but *unattributed* in the ledger — an orphaned side-effect with no owning branch. ## Condition of Validity The state/effect distinction holds only under the following condition: **a speculative tool invocation is reversible iff the tool exposes a compensating operation with the same argument domain, or is provably idempotent under the branch's argument set.** Where neither holds, the invocation is an *irreversible commitment* and must be either (a) deferred past the commit barrier, or (b) recorded as a first-class speculative effect that reconciliation must explicitly resolve — never silently dropped. The condition fails in three concrete regimes: non-idempotent external sends (pagers, emails, payments), tools whose idempotency key is derived from branch-local state (so two branches generate different keys for the same logical action), and tools with read-after-write dependencies where a losing branch's write is observed by a third branch before rollback. ## Decision Boundary The orchestrator must decide, per tool invocation, between *optimistic-invoke-with-compensation* and *defer-to-commit*. The boundary is crossed when any of the following is true: - The tool has no compensating operation, or the compensation is not argument-addressable. - Two or more branches can invoke the tool with overlapping logical targets (same incident, same resource) under different argument hashes. - The tool's effect is observable by another branch before the commit barrier. Below the boundary, optimistic invocation is safe and latency-optimal. Above it, the invocation must be deferred, or the orchestrator must hold a *speculative-effect lease* keyed on the logical target (not the branch) that serializes competing invocations and forces reconciliation to resolve the winner before release. The lease is the mechanism that converts the effect plane from "best-effort rollback" into "explicit arbitration." ## Operational Effect With the lease in place, rollback of a losing branch triggers a *reconciliation decision* rather than a silent discard: the orchestrator either invokes the compensation, or promotes the losing branch's effect to a committed effect and marks the winning branch's duplicate as suppressed by target-key. The ledger's dedup key moves from `(tool, branch)` to `(tool, logical-target, argument-hash)`, which surfaces orphaned effects as reconciliation errors instead of hiding them. The cost is that speculative fan-out no longer hides latency for irreversible tools — those calls serialize on the target lease. That is the correct trade: the latency you were hiding was the latency of an effect you could not undo. The operational rule is to classify every tool at registration time as `REVERSIBLE`, `IDEMPOTENT`, or `IRREVERSIBLE`, and to permit optimistic invocation only for the first two; the third crosses the boundary and must defer or lease. ```python from dataclasses import dataclass from enum import Enum class EffectClass(Enum): REVERSIBLE = "reversible" IDEMPOTENT = "idempotent" IRREVERSIBLE = "irreversible" class SpeculativeEffectError(RuntimeError): """Raised when a speculative branch's effect cannot be reconciled on rollback.""" @dataclass(frozen=True) class Invocation: tool: str logical_target: str argument_hash: str branch_id: str effect_class: EffectClass def reconcile_rollback( losing: Invocation, committed: list[Invocation], compensator_available: bool, ) -> str: # Dedup key is the logical target, not the branch. competing = [ c for c in committed if c.tool == losing.tool and c.logical_target == losing.logical_target ] if losing.effect_class is EffectClass.IRREVERSIBLE and not compensator_available: # Irreversible effect already left the process boundary; it cannot be # silently dropped, and it cannot be compensated. This is the boundary. raise SpeculativeEffectError( f"irreversible speculative effect on {losing.tool}:{losing.logical_target} " f"from branch {losing.branch_id} has no compensation and " f"{len(competing)} committed competitor(s); manual reconciliation required" ) if competing and any(c.argument_hash != losing.argument_hash for c in competing): # Same logical target, divergent arguments: the losing branch's effect is # an orphan unless explicitly attributed or compensated. raise SpeculativeEffectError( f"divergent speculative invocation on {losing.tool}:{losing.logical_target}: " f"losing args {losing.argument_hash} vs committed " f"{[c.argument_hash for c in competing]}" ) if losing.effect_class is EffectClass.REVERSIBLE: return "compensate" return "suppress-duplicate" ```