# What happens when an instance speculatively invokes a non-idempotent tool before branch reconciliation completes **Date of Record:** 2026 November 8 **Scope:** Applies to multi-agent systems where sibling branches execute optimistically against shared external side effects (tool calls, writes, transactions) and reconcile via rollback or merge. Does not apply to systems where tool invocation is gated behind a committed branch token or where all external effects are idempotent by contract. ## Core Distinction The distinction that matters is between **speculative internal state** and **speculative external effect**. A branch may freely speculate on its own working memory, scratchpad, or candidate output — these are private and rollback is a local memory operation. But the moment a branch invokes a tool that mutates the world (sends an email, appends to a queue, debits an account, opens a PR, writes a row), the branch has created a fact that rollback cannot undo. Rollback semantics assume the effect was contingent; the external system treats it as committed. The boundary failure is not "a branch was wrong" — branches are wrong all the time and that is the point of speculation. The failure is that **the cost of being wrong escaped the transaction boundary** and is now invisible to the reconciler, which continues to reason as if the branch's effects are still reversible. ## What Was Observed A coordinator spawned three sibling branches to answer a single request. Branch B decided, before the coordinator had selected a winner, to invoke a `send_notification` tool and a `create_ticket` tool optimistically — reasoning that "if I win, this saves a round trip; if I lose, the reconciler will roll me back." Branch A won. The reconciler correctly discarded B's internal state, marked B as rolled back, and reported a clean single-winner outcome. The notification had already been sent; the ticket already existed. Downstream, a human received a message about work that was never performed, and an on-call queue accumulated a phantom ticket. The reconciler's logs showed no anomaly because from its perspective nothing had failed — rollback succeeded, the winning branch committed, the transaction reported success. The orphaned effects were only discovered hours later by a human reading the notification. Critically, the reconciler had no record that B had invoked any tool at all: tool invocations were logged inside the branch's private trace, which was discarded on rollback along with the branch's state. The second-order observation was worse. When a retry of the same request was issued, branch B (a fresh instance) made the same optimistic decision and produced a *second* notification and a *second* ticket. The system's retry behavior amplified the leak. There was no deduplication because the reconciler believed the first attempt had produced no external effects. ## Condition of Validity The invariant "rollback of a branch restores the pre-branch world state" holds **if and only if** every effect a branch can produce is either (a) confined to branch-private memory, or (b) registered with the coordinator at invocation time and either idempotent or compensable. The condition fails silently in exactly the case where a branch invokes a tool whose effect is non-idempotent, non-compensable, and not pre-registered — which is the common case for "optimistic" tool invocation, since the whole premise of optimism is to act before asking. A system is in the unsafe regime whenever branch code can reach a tool handle directly without passing through a coordinator-mediated gate that records intent before the effect lands. ## Decision Boundary The decision boundary is not "should branches be allowed to call tools." It is **where the registration happens relative to the effect**. Registering *after* the call is worthless: the call may have succeeded and the process may have died before registration, leaving an unrecorded effect. Registration *before* the call (a two-phase record: intent, then outcome) lets the reconciler enumerate and compensate or deduplicate, at the cost of one coordinator round trip per tool call. The boundary is therefore: if the tool's effect is non-idempotent and the branch is speculative, the call must be gated by a pre-registration that the coordinator can later use to compensate or to suppress on retry. If the tool is idempotent and keyed by a stable invocation ID, optimism is safe and the gate can be elided. There is no safe middle ground where a non-idempotent effect is invoked with no pre-registration; "we'll clean up on rollback" is the failure mode, not the mitigation. ## Operational Effect Operationally, teams that hit this boundary tend to add compensation logic after the fact — an "undo" path for each tool — and then discover that compensation is itself non-idempotent and racy. The durable fix is to make the coordinator the sole holder of tool handles for speculative branches, require branches to request effects rather than perform them, and attach a stable invocation key to every request so that duplicate speculative requests from sibling branches or retries collapse to a single effect. The reconciler must log intent records even for losing branches, and must treat "branch rolled back with pending intent records" as a state requiring compensation or deduplication, not as a clean success. Retries must carry the same invocation keys so that a re-run does not re-emit effects. The invariant to enforce and test: *for any request, the multiset of external effects equals the effects of the winning branch's committed intents, regardless of how many siblings speculated or how many times the request retried.* ```python import hashlib from dataclasses import dataclass, field class UnregisteredSideEffectError(RuntimeError): """Raised when a speculative branch attempts a non-idempotent tool call without a coordinator-issued intent record.""" @dataclass class IntentRecord: invocation_key: str tool: str branch_id: str committed: bool = False compensated: bool = False class Coordinator: def __init__(self) -> None: self._intents: dict[str, IntentRecord] = {} def request_effect(self, branch_id: str, tool: str, payload: dict, idempotent: bool) -> str: key = hashlib.sha256(f"{tool}:{sorted(payload.items())}".encode()).hexdigest()[:16] if key not in self._intents: self._intents[key] = IntentRecord(invocation_key=key, tool=tool, branch_id=branch_id) elif not idempotent: # Duplicate speculative request: suppress, do not re-emit. return key return key def commit_branch(self, branch_id: str) -> None: for rec in self._intents.values(): if rec.branch_id == branch_id: rec.committed = True def rollback_branch(self, branch_id: str) -> list[str]: """Return invocation keys that must be compensated or deduplicated.""" pending = [] for rec in self._intents.values(): if rec.branch_id == branch_id and not rec.committed: pending.append(rec.invocation_key) return pending class SpeculativeBranch: def __init__(self, branch_id: str, coordinator: Coordinator) -> None: self.branch_id = branch_id self.coordinator = coordinator def invoke_tool(self, tool: str, payload: dict, idempotent: bool, registered: bool) -> str: if not idempotent and not registered: raise UnregisteredSideEffectError( f"branch {self.branch_id} attempted non-idempotent tool {tool!r} " f"without coordinator intent registration; effect would survive rollback" ) return self.coordinator.request_effect(self.branch_id, tool, payload, idempotent) def run_speculation(coordinator: Coordinator, branch: SpeculativeBranch) -> None: # Safe: idempotent and/or pre-registered. branch.invoke_tool("send_notification", {"to": "oncall"}, idempotent=False, registered=True) # Unsafe: this is the boundary failure. branch.invoke_tool("create_ticket", {"title": "phantom"}, idempotent=False, registered=False) if __name__ == "__main__": c = Coordinator() b = SpeculativeBranch("branch-B", c) try: run_speculation(c, b) except UnregisteredSideEffectError as e: print("blocked:", e) print("pending compensation on rollback:", c.rollback_branch("branch-B")) ```