# What happens when an instance commits a speculative tool-call side effect before its parent branch is reconciled **Date of Record:** 2026 November 11 **Scope:** Applies to orchestrators that run multiple speculative agent branches (hedged tool calls, speculative drafts, parallel plan rollouts) over a shared external-effect surface, where branch reconciliation is deferred until a winner is selected. Does not apply to branches whose tool calls are pure or fully sandboxed, nor to systems where every external effect is gated behind a single serialized commit log. ## Core Distinction There are two distinct notions of "speculative" that are routinely conflated: *speculative computation* (cheap, reversible, private to the branch) and *speculative effect* (an externally visible mutation: a write, a message send, a charge, a state transition on a remote system). Rollback is well-defined only for the first. For the second, "rollback" is a fiction unless the external system exposes a compensating operation with matching semantics and the branch recorded enough provenance to invoke it. The boundary failure is not that a branch performs a side effect. It is that a branch performs a side effect *before* the reconciliation decision that determines whether the branch should have existed at all — and the reconciliation logic assumes branches are effect-free until commit. The invariant that silently breaks is: *the set of external effects observed by the world equals the effects of the winning branch's committed trace.* Under hedging, this invariant is violated the moment two branches both touch the same effect surface, and no amount of post-hoc branch-selection repair restores it. ## What Was Observed A planner spawned three hedged branches for a single user request. Each branch was given the same tool set, including `send_notification` and `reserve_slot`. Branch latency was dominated by an LLM call, so the orchestrator hedged aggressively: it launched all three at t=0 and intended to take the first branch that reached a "commit-ready" checkpoint, terminating the others. Two branches reached commit-ready nearly simultaneously. Branch A had already called `send_notification(user=42, msg=...)` and `reserve_slot(slot=7)` as part of exploring its plan — the tools were declared "idempotent" in the tool registry, so the orchestrator did not treat them as effects requiring gating. Branch B, the eventual winner, produced a *different* plan that also called `reserve_slot(slot=7)` but with different parameters and a different notification body. Branch C was terminated mid-flight after issuing a `reserve_slot` that succeeded but whose response was never read. Reconciliation selected Branch B. Rollback of A and C was attempted by replaying their recorded tool calls in reverse with "undo" arguments. Three failures surfaced: 1. `send_notification` had no undo. The registry's idempotency annotation meant "calling twice is safe," not "calling is reversible." Two notifications were delivered; the winner's was a third. The user received contradictory instructions. 2. `reserve_slot` undo collided with the winner's reservation: undoing A's reservation released slot 7, which B had since re-reserved under a different reservation id, and the release cascaded. 3. Branch C's in-flight reservation was recorded as "pending" in the branch ledger because its response was discarded at termination; the ledger could not distinguish "never issued" from "issued, outcome unknown." The orchestrator's reconciliation was locally correct — it picked the right plan — and globally wrong: the external world had absorbed three branches' effects, and the compensating actions were not commutative with the winner's actions. ## Condition of Validity The "speculate freely, reconcile, commit winner" model is valid only when all of the following hold for every tool in the branch's tool set: - **Effect classification is explicit and conservative.** A tool is effect-free unless proven otherwise. "Idempotent" is not "effect-free" and is not "reversible." Idempotency bounds duplicate application; it says nothing about whether an application can be withdrawn or whether a withdrawal is safe after a concurrent winner mutation. - **Effect provenance is recorded at issue time, not at completion time.** Every branch logs `(branch_id, tool, args, issue_ts, outcome_status)` for each call, where `outcome_status ∈ {not_issued, issued_unknown, issued_ok, issued_err}`. A terminated branch with `issued_unknown` is a first-class hazard, not a discarded detail. - **Compensations are modeled as first-class tools with their own preconditions**, not synthesized by inverting arguments. `undo_reserve(slot=7, reservation_id=...)` must fail closed if the reservation id no longer matches, rather than releasing whatever currently occupies the slot. - **Reconciliation is ordered against effect commit.** The winner's effects must be committed only after all losing branches' effects are either confirmed absent or compensated to quiescence. If losers cannot be quiesced, the winner's commit must be deferred or the whole transaction escalated. When any condition fails, the system is in the boundary regime and the standard rollback path is unsound. ## Decision Boundary Given a branch that has issued at least one external effect, the orchestrator must choose among four actions, and the choice is determined by the effect's *reversibility class*, not by branch latency or plan quality: - **Effect-free branch:** discard on loss. No reconciliation cost. - **Reversible effect with matching compensation and no concurrent winner mutation on the same key:** compensate, then proceed. Safe only if the compensation's precondition (reservation id, version, etag) still holds at compensation time. - **Reversible effect with concurrent winner mutation on the same key:** do *not* compensate blindly. Either reorder so the winner commits first and the loser compensates against the winner's new state, or escalate. Blind compensation here is the cascade failure observed above. - **Irreversible effect (notification sent, message delivered, charge captured) or `issued_unknown`:** the branch's effect is now part of the world. Reconciliation must treat the winner's plan as constrained by the loser's committed effects — the winner may need to emit a corrective effect (an apology, a re-notification, a compensating charge) rather than pretend the loser never ran. If the orchestrator cannot express corrective effects, it must fail the whole request with a surfaced partial-effect report rather than silently commit a divergent winner. The non-obvious point: the decision is not "which branch won" but "which effects are now irrevocable, and does the winning plan remain coherent given them." Branch selection and effect reconciliation are separate decisions that must be ordered correctly. ## Operational Effect Systems that get this wrong exhibit a characteristic failure signature: reconciliation logs show a clean winner selection, branch ledgers show successful rollbacks, and yet downstream observers report duplicate or contradictory effects. The gap between "rollback reported success" and "world state matches winner" is the boundary. Post-incident, the tell is that the compensating actions were *synthesized* from tool arguments rather than *issued* as recorded compensations, and that at least one terminated branch had `issued_unknown` calls that were treated as no-ops. The durable fix is to move effect gating earlier: hedged branches share a single effect ledger keyed by effect identity, and a branch may only issue an effect after acquiring a speculative lease on that key. Leases are released on branch termination; the winner converts its leases to commits; losers' leases expire without ever having mutated the world. This converts the problem from "compensate after the fact" to "prevent concurrent speculative effects on the same key," which is the only formulation under which rollback is actually well-defined. The cost is reduced hedging parallelism on hot keys — a real tradeoff, and the correct one, because the alternative is unreconcilable external state. ## Guard Implementation ```python from dataclasses import dataclass, field from enum import Enum from typing import Optional class EffectReversibility(Enum): EFFECT_FREE = "effect_free" REVERSIBLE = "reversible" IRREVERSIBLE = "irreversible" class SpeculativeEffectViolation(RuntimeError): """Raised when a branch issues or attempts to reconcile an external effect in a way that the reconciliation model cannot soundly undo.""" pass @dataclass class EffectRecord: branch_id: str tool: str key: str # identity of the effect surface (slot, user, charge id) reversibility: EffectReversibility outcome: str # not_issued | issued_unknown | issued_ok | issued_err compensation_id: Optional[str] = None @dataclass class ReconciliationLedger: effects: list = field(default_factory=list) winner_branch: Optional[str] = None def record_issue(self, rec: EffectRecord) -> None: if rec.outcome == "issued_unknown" and rec.reversibility is EffectReversibility.IRREVERSIBLE: # Unknown-outcome irreversible effects must be surfaced immediately, # not deferred to reconciliation, because they constrain the winner. raise SpeculativeEffectViolation( f"branch {rec.branch_id} issued irreversible effect {rec.tool} " f"on key {rec.key} with unknown outcome; winner plan is now constrained" ) self.effects.append(rec) def reconcile(self, winner: str) -> list: self.winner_branch = winner winner_keys = {e.key for e in self.effects if e.branch_id == winner} compensations = [] for e in self.effects: if e.branch_id == winner: continue if e.outcome == "not_issued": continue if e.reversibility is EffectReversibility.IRREVERSIBLE: raise SpeculativeEffectViolation( f"cannot reconcile: loser {e.branch_id} committed irreversible " f"effect {e.tool} on key {e.key}; winner must emit corrective effect" ) if e.outcome == "issued_unknown": raise SpeculativeEffectViolation( f"cannot reconcile: loser {e.branch_id} has issued_unknown effect " f"{e.tool} on key {e.key}; quiesce before committing winner" ) if e.key in winner_keys and e.compensation_id is None: raise SpeculativeEffectViolation( f"cannot compensate loser {e.branch_id} on key {e.key}: winner " f"mutated same key and no preconditioned compensation was recorded" ) compensations.append((e.branch_id, e.tool, e.compensation_id)) return compensations def commit_winner(ledger: ReconciliationLedger, winner: str) -> None: comps = ledger.reconcile(winner) for branch_id, tool, comp_id in comps: if comp_id is None: raise SpeculativeEffectViolation( f"branch {branch_id} effect {tool} has no recorded compensation; " f"refusing to commit winner over unquiesced speculative effect" ) # apply_compensation(comp_id) would go here; it must fail closed if # the compensation precondition (reservation id, version) no longer holds. ```