# When must a speculative multi-agent executor abort a branch instead of reconciling it? **Date of Record:** 2026 November 6 **Scope:** Applies to executors that run multiple agent branches speculatively against a shared tool layer, where each branch may emit irreversible side effects (writes, sends, charges) and branches are later reconciled or rolled back. Does not apply to purely read-only fan-out or to branches whose tool calls are all idempotent and transactional. ## Core Distinction There are two failure classes that look identical in a trace but demand opposite responses: 1. **Divergence** — branches produced different *results* from the same pre-state. Divergence is reconcilable: you can pick a winner, merge, or re-run. 2. **Contamination** — a branch's speculative tool call *mutated shared state* that another branch (or the reconciler itself) subsequently read. Contamination is not reconcilable, because the branches no longer share a common ancestor state. Rollback of the branch's *log* does not roll back the *observation* other branches already made. The non-obvious invariant: **reconciliation is only valid over branches whose speculative reads were all served from the same snapshot.** The moment a speculative tool call escapes the sandbox and is observed by a sibling branch, the branch set is contaminated and must be aborted, not merged. Executors routinely get this wrong because they track *writes* for rollback but not *read-after-speculative-write* edges between siblings. ## What Was Observed A three-branch executor (branches `A`, `B`, `C` sharing a tool layer) was asked to plan and execute a resource reservation. Branch `A` speculatively called `reserve(slot=7)` against the live tool (the tool was assumed idempotent and reversible). The call succeeded and wrote to the shared reservation table. Branch `B`, running concurrently, read the reservation table to check availability and observed `slot=7` as taken — a state that exists *only because of A's speculation*. Branch `B` then chose a different slot and emitted its own `reserve(slot=9)`. The reconciler, seeing `A` and `B` disagree on slot, treated it as ordinary divergence, selected `B` (higher confidence), and rolled back `A`. The rollback removed `A`'s reservation but left `B`'s choice intact — and `B`'s choice was *causally downstream of a state that no longer exists*. The final committed plan was justified by a premise (slot 7 occupied) that the system had itself retracted. The result was a reservation at slot 9 that no branch would have chosen from the true pre-state. No error was raised; the reconciler reported success. The tell in the trace was a read on `B` whose value matched `A`'s speculative write, timestamped between `A`'s write and `A`'s rollback. The executor had no edge for this because it modeled branches as isolated writers, not as readers of each other's speculation. ## Condition of Validity A speculative branch may be **reconciled** only if all of the following hold for the entire branch set: - Every speculative tool call is either (a) routed to a per-branch overlay/snapshot, or (b) proven side-effect-free and non-observable by siblings. - No sibling branch performed a read whose result depends on another branch's speculative write (no cross-branch read-after-speculative-write edge). - The reconciler's own inputs were computed from the pre-speculation snapshot, not from live state polluted by any branch. If any condition is unverifiable — e.g., the tool layer cannot report read provenance, or a tool's idempotency is assumed rather than enforced — the branch set must be treated as potentially contaminated. Assumed idempotency is not a condition of validity; it is a hope. The only safe default when provenance is unknown is abort. ## Decision Boundary The executor must choose **abort** (discard all branches, re-run from the last clean snapshot) rather than **reconcile** when: - A cross-branch read-after-speculative-write edge is detected, *or* - A speculative tool call cannot be proven overlay-isolated and has already executed against shared state, *or* - The reconciler cannot establish that its comparison inputs predate all speculative writes. It may choose **reconcile** only when the branch set is snapshot-clean per the section above. Partial contamination is total: if one branch contaminated the set, sibling branches that *appear* clean may still have read the contaminated state, so the abort scope is the whole set sharing that tool layer, not the offending branch alone. Narrowing the abort to the writer is the single most common error and reproduces the observed failure. The cost asymmetry justifies the strictness: a false abort costs a re-run; a false reconcile commits a plan whose justification the system has retracted, silently. ## Operational Effect Enforcing this boundary changes the executor in three ways. First, the tool layer must expose read provenance (which snapshot served each read) or route speculation through per-branch overlays; without this the boundary degrades to "always abort," which is correct but expensive. Second, the reconciler must run against the pre-speculation snapshot and be forbidden from reading live state. Third, rollback must be scoped to the contamination set, and the executor must emit an explicit `ContaminationAbort` rather than a divergence record, so downstream retry logic re-runs from a clean snapshot instead of attempting a merge. The invariant to encode: *a branch that read another branch's speculation is not a branch — it is a continuation of that speculation, and dies with it.* ```python class ContaminationAbort(RuntimeError): """Raised when a speculative branch set cannot be safely reconciled.""" pass def reconcile_branches(branches, tool_layer, base_snapshot): # Every read must be served from base_snapshot; every speculative # write must be overlay-isolated. Anything else is contamination. for branch in branches: for read in branch.reads: if read.snapshot_id != base_snapshot.id: raise ContaminationAbort( f"branch {branch.id} read from snapshot {read.snapshot_id}, " f"not base {base_snapshot.id}; set is contaminated" ) if read.observed_speculative_write_from is not None: raise ContaminationAbort( f"branch {branch.id} observed speculative write from " f"{read.observed_speculative_write_from}; abort whole set" ) for call in branch.speculative_calls: if call.executed and not tool_layer.is_overlay_isolated(call): raise ContaminationAbort( f"branch {branch.id} call {call.tool} escaped overlay " f"and is not provably non-observable; abort whole set" ) # Only reached when the set is snapshot-clean. return _merge(branches) ```