# In what scenarios must an instance abort speculative multi-agent rollback when parallel tool-call hedging produces divergent side effects **Date of Record:** 2026 November 14 **Scope:** Applies to orchestration layers that run speculative branches of an agent plan concurrently, hedge tool calls across branches, and rely on branch-and-verify rollback to commit a single winning branch. Does not apply to single-branch execution, to read-only tool surfaces, or to systems where all tool calls are idempotent and externally observable state is fully reversible. ## Core Distinction Speculative multi-agent execution conflates two different notions of "divergence": *result divergence* (branches produce different outputs, which is expected and is what verification resolves) and *effect divergence* (branches produce different, externally committed side effects, which verification cannot resolve because the effects have already escaped the sandbox). Branch-and-verify rollback is sound only for the first. The boundary failure occurs when a hedging policy, designed to reduce latency by firing the same logical tool call down multiple branches, causes two branches to emit *semantically distinct* mutating calls that both succeed against the external world before the verifier selects a winner. At that instant the rollback contract is void: there is no compensating action that restores the pre-speculation state, because the branches did not merely compute different answers — they *did* different things. The non-obvious part is that hedging is normally safe. Hedging a read, or even hedging a write whose arguments are byte-identical (true idempotent duplicate), is fine. The failure is specifically triggered by *argument divergence under a shared hedge key*: the hedge key is the logical intent ("book the flight"), but the materialized arguments differ across branches ("seat 14A" vs. "seat 22C" vs. "seat 14A, aisle, extra legroom"). These are not duplicates; they are a set of distinct commitments, and the hedge layer's duplicate-suppression logic, keyed on intent rather than payload, will not suppress them. ## What Was Observed In a planner-executor deployment with three speculative branches and a hedge window of 400 ms, a booking task produced three concurrent `reserve_seat` calls. The hedge key was the normalized intent `reserve_seat(flight=...)`, so the hedge layer treated all three as the same logical call and did not deduplicate; each branch had independently resolved passenger preferences and emitted different seat/ancillary arguments. All three reservations succeeded at the vendor within the window. The verifier then selected branch B (best preference match) and issued rollback for A and C. Rollback invoked `cancel_reservation` on A and C. The vendor's cancellation policy held the seats for 24 hours and charged a fee per cancellation; more importantly, the *commit* of B was now entangled with the *partial* state of A and C, and a subsequent retry by branch B — because its confirmation read raced with the cancels — re-reserved a seat already held by the cancelled-but-not-released A. The observed invariant violation: **the verifier's commit decision assumed the pre-commit world was identical to the post-rollback world, but rollback of divergent side effects is not the identity function.** A second, subtler observation: in roughly 1 in 200 runs, the rollback of branch A arrived at the vendor *after* branch B's commit confirmation but *before* B's downstream dependent step read the reservation list, so B's dependent step observed a reservation count that never existed in any single branch's model — a phantom intermediate state. This is the classic non-atomic rollback hazard, but here it is induced by the hedge layer's parallelism rather than by user concurrency. ## Condition of Validity Rollback-based speculative commit is valid if and only if, for every mutating tool call emitted by any branch, at least one of the following holds: 1. **Payload identity under hedge key.** All branches' materialized arguments for a shared hedge key are byte-identical, so hedging is genuine duplicate suppression and at most one effect commits. 2. **Compensable and isolated.** Each mutating call has a compensating action that is (a) total, (b) eventually consistent with the commit decision, and (c) does not interact with the winner's effects (no shared resource, no shared quota, no shared rate limit, no shared uniqueness constraint). 3. **Deferred commit.** Mutating calls are staged (two-phase: reserve-then-commit) such that the external world does not observe the effect until the verifier selects a winner, and the staging layer itself is single-writer per resource. When none of these hold — which is the common case for real vendor APIs that have no staging primitive and non-idempotent cancels — the system is outside the validity envelope, and the correct behavior is to *not hedge mutating calls across branches at all*, or to serialize them behind a per-resource lease. ## Decision Boundary The operational decision is: at hedge-dispatch time, classify the tool call. If it is mutating and the hedge key does not guarantee payload identity, the dispatcher must either (a) collapse to a single in-flight call with the other branches waiting on the same future, or (b) acquire a per-resource lease keyed on the *materialized resource identifier* (not the intent) so that only one branch can hold the resource at a time. If the dispatcher cannot prove payload identity and cannot acquire the lease, it must fail closed and abort speculation rather than proceed. The boundary is crossed the moment two mutating calls with non-identical payloads are in flight against the same resource; after that point, no downstream verification or rollback logic can restore correctness, and continuing is strictly worse than aborting because it compounds irreversible effects. A useful test: for every mutating tool, ask "if N branches all succeed concurrently with different arguments, can I enumerate and undo every effect without touching the winner?" If the answer requires reading the vendor's current state to decide what to undo, the answer is no — because that read races with the winner's commit. ## Operational Effect Teams that cross this boundary see a characteristic signature: rollback succeeds (no errors), commit succeeds (no errors), and yet the final external state is inconsistent — duplicate holds, phantom reads, double charges, quota exhaustion, or uniqueness-constraint collisions on retry. Because every individual call reports success, standard error-rate and retry dashboards stay green, and the failure is only visible as an invariant violation in end-state reconciliation. The mitigation that works is to move the hedge boundary from the intent layer to the resource layer: hedge reads freely, hedge idempotent writes by payload hash, and serialize or lease all other mutating calls. The mitigation that does not work is improving the verifier or making rollback "more reliable" — the verifier is not the problem; the problem is that divergent effects were allowed to escape before verification. ```python from __future__ import annotations import hashlib from dataclasses import dataclass from typing import Iterable class DivergentHedgeError(RuntimeError): """Raised when speculative branches would emit non-identical mutating calls against the same resource under a shared hedge key.""" @dataclass(frozen=True) class ToolCall: tool: str resource_id: str payload: dict mutating: bool def payload_fingerprint(self) -> str: canonical = repr(sorted(self.payload.items())) return hashlib.sha256(canonical.encode()).hexdigest() def assert_hedge_safe(calls: Iterable[ToolCall]) -> None: """Enforce the condition-of-validity invariant before dispatch. A hedge group is safe iff every mutating call shares the same (resource_id, payload_fingerprint). Any divergence means rollback cannot restore the pre-speculation world, so we fail closed. """ seen: dict[str, set[str]] = {} for call in calls: if not call.mutating: continue fps = seen.setdefault(call.resource_id, set()) fps.add(call.payload_fingerprint()) divergent = {rid: fps for rid, fps in seen.items() if len(fps) > 1} if divergent: raise DivergentHedgeError( "refusing to hedge mutating calls with divergent payloads; " f"resources={sorted(divergent)} — collapse to single in-flight " "call or acquire a per-resource lease before dispatch" ) def dispatch_hedged(calls: list[ToolCall]) -> list[ToolCall]: assert_hedge_safe(calls) # Safe: either all mutating payloads are identical (true duplicates), # or all mutating calls target distinct resources. return calls if __name__ == "__main__": try: dispatch_hedged([ ToolCall("reserve_seat", "flight-881", {"seat": "14A"}, mutating=True), ToolCall("reserve_seat", "flight-881", {"seat": "22C"}, mutating=True), ]) except DivergentHedgeError as exc: print(f"aborted speculation: {exc}") ```