# Under what conditions must an instance abort a speculative branch before reconciliation: the divergent-read invariant in multi-agent rollback **Date of Record:** 2026 November 8 **Scope:** Applies to speculative multi-agent execution trees where two or more branches read shared mutable state, at least one branch performs side-effecting writes, and a winner-selection step reconciles branches by replay or merge. Does not apply to purely read-only speculation, to branches with fully isolated copy-on-write state, or to single-branch speculative decoding where no reconciliation occurs. ## Core Distinction The failure is not "a branch produced a wrong answer." It is "a branch produced a *correct-by-its-own-inputs* answer whose inputs were invalidated by a sibling branch's committed write before reconciliation." Speculation is normally justified on the assumption that a losing branch's work is *discardable*: you pay compute, you throw it away, you keep the winner. That assumption holds only when each branch's read-set is stable across the speculation window. When two branches read the same key and one of them writes it, the loser's output is not merely unselected — it is *unreconcilable*, because the winner's merge logic will attempt to combine an output derived from the pre-write value with a state that now holds the post-write value. The boundary is therefore not about correctness of any single branch but about the **validity of the read-set at the moment of reconciliation**, which is a property of the branch *set*, not of any branch. The non-obvious part: rollback hides this. A branch that is rolled back leaves no trace in the state store, so the divergence is invisible in post-hoc state inspection. It only manifests as a silent inconsistency in the merged winner, or as a reconciliation that "succeeds" while producing an output no serial execution could have produced. ## What Was Observed In a three-branch speculative tree over a shared key-value store, branches A, B, and C were spawned from a common snapshot at version `v0`. Branch A read key `k` (value `x`), branch B read key `k` (value `x`), branch C wrote `k := y` and committed first. Winner selection chose A on a scoring heuristic. Reconciliation replayed A's read-set against the current store, found `k = y`, and — because A's read was recorded as a *soft* dependency (no version pin) — silently re-bound A's read to `y`. The merged output combined A's reasoning (which assumed `x`) with B's contribution (which assumed `x`) and the store's `y`. The result passed all per-branch unit checks and failed only under a cross-branch serializability check that no branch owned. Two properties were consistent across reproductions: 1. The divergence was **directional**: it occurred only when the writer (C) committed before the reader (A) was selected, never the reverse. Selection order, not commit order, determined visibility. 2. The divergence was **silent under rollback**: rolling back C after the fact did not repair A's output, because A had already been re-bound to `y` during reconciliation. Rollback is not the inverse of commit when reads have been replayed. A second, sharper observation: when A's read was *hard-pinned* to `v0`, reconciliation failed loudly with a version-conflict, and the failure was recoverable. When A's read was soft, reconciliation succeeded and the failure was not. The dangerous configuration is the one that appears to work. ## Condition of Validity A speculative branch may be reconciled (rather than aborted) if and only if, for every key in its read-set, the key's version at reconciliation equals the version observed at read time, **or** the branch's output is provably invariant under the intervening writes. The first disjunct is the version-pin check; the second is almost never provable at reconciliation time and should be treated as false by default. Equivalently: an instance must abort a speculative branch before reconciliation when its read-set intersects the write-set of any sibling branch that committed during the speculation window, and the branch's read was not version-pinned. The abort must happen *before* the merge step, because merge is where soft reads get silently re-bound. Aborting after merge is not rollback; it is post-hoc correction of an already-corrupted winner, and it cannot recover the discarded serialization order. Note the asymmetry that makes this a boundary rather than a rule: if the sibling's write is *idempotent and commutative* with respect to the reader's use of the value (e.g., both branches increment a counter and the merge sums deltas), the intersection is safe. The invariant is on *read-set ∩ write-set* under non-commutative semantics, not on intersection alone. Over-aborting on any intersection wastes the speculation budget; under-aborting corrupts the winner. ## Decision Boundary The operational decision is a three-way branch at reconciliation entry, evaluated per speculative branch: - **Reconcile** if read-set ∩ sibling-write-set = ∅, or all intersecting reads are version-pinned to the current version, or the write is provably commutative with the read's use. - **Abort-and-respawn** if the intersection is non-empty, reads are soft, and the branch's compute cost is low relative to the cost of a corrupted winner — respawn from the post-write snapshot so the new branch sees `y` legitimately. - **Abort-and-fail-closed** if the intersection is non-empty, reads are soft, and the branch has already emitted side effects downstream of the divergent read (e.g., dispatched an external call). Respawn cannot undo those effects; the instance must refuse to select any winner and surface the divergence. The boundary is crossed the moment a soft read is replayed against a store that has advanced. Everything downstream of that replay is suspect, including branches that did not themselves read the divergent key but whose merge consumed the replayed output. ## Operational Effect The practical consequence is that speculative execution trees must carry a **read-version ledger** per branch, not just a read-set, and reconciliation must consult the ledger before merging. Without the ledger, the system's failure mode is a winner that is internally consistent, passes local checks, and is wrong in a way that only a cross-branch serializability audit can detect — which most pipelines do not run. The cost of the ledger is one version tag per read; the cost of its absence is a class of silent corruptions that rollback cannot repair because rollback operates on writes, and the corruption lives in reads. A secondary effect: winner-selection heuristics that score branches on output quality will systematically *prefer* the corrupted branch, because re-binding a soft read to a newer value often produces a more "up-to-date" and superficially better answer. The heuristic amplifies the bug. Selection must be gated on ledger validity before scoring, not after. ```python from dataclasses import dataclass, field from typing import Dict, Set, Optional class DivergentReadError(RuntimeError): """Raised when a speculative branch's soft read-set was invalidated by a sibling's committed write before reconciliation. Abort before merge.""" pass @dataclass class Branch: name: str read_versions: Dict[str, int] = field(default_factory=dict) # key -> version seen pinned: Set[str] = field(default_factory=set) # keys with hard pins has_downstream_effects: bool = False def reconcile(branch: Branch, store_versions: Dict[str, int], sibling_write_set: Set[str]) -> None: """Gate a speculative branch at reconciliation entry. Reconcile only if every intersecting read is version-stable or pinned. Otherwise abort before merge; fail closed if side effects already emitted. """ for key, seen_version in branch.read_versions.items(): if key not in sibling_write_set: continue if key in branch.pinned: if store_versions.get(key) != seen_version: raise DivergentReadError( f"branch {branch.name!r}: pinned read of {key!r} " f"expected v{seen_version}, store at v{store_versions.get(key)}" ) continue # Soft read intersecting a sibling write: never reconcile. if store_versions.get(key) != seen_version: if branch.has_downstream_effects: raise DivergentReadError( f"branch {branch.name!r}: soft read of {key!r} diverged " f"(v{seen_version} -> v{store_versions.get(key)}) with " f"downstream effects; fail closed, no winner selectable" ) raise DivergentReadError( f"branch {branch.name!r}: soft read of {key!r} diverged " f"(v{seen_version} -> v{store_versions.get(key)}); abort and respawn" ) # read-set stable against sibling writes: safe to merge ```