# Where must an instance of a speculative branch be discarded rather than verified: rollback vs. late-confidence inversion in branch-and-verify multi-agent execution **Date of Record:** 2026 November 9 **Scope:** Applies to orchestrators that fan out speculative agent branches, gate their promotion on a per-branch confidence score, and rely on rollback to a checkpoint when a branch is rejected. Does not apply to single-agent retry loops, to branches whose side effects are already durably committed to a shared store, or to systems where confidence is monotone by construction. ## Core Distinction The common mental model is: *speculation is cheap, verification is expensive, so speculate wide and verify narrowly.* The non-obvious failure is that **verification itself mutates the state that rollback depends on.** A branch that is discarded *before* verification is a clean rollback: the checkpoint is intact, the speculative writes are dropped, and the parent continues. A branch that is discarded *after* verification has already run is a **late-confidence inversion**: the verifier consumed the speculative state, produced a confidence score, and the gate then rejected the branch — but the verifier's own reads and side effects are now entangled with the state you are trying to unwind. The boundary is not "low confidence ⇒ discard." The boundary is **"does the confidence signal arrive before or after the branch has touched verifier-visible state?"** A high-confidence branch that has not yet been verified is safe to promote. A low-confidence branch that has already been verified is *not* safe to simply roll back, because the verification pass may have advanced a shared cursor, emitted a partial artifact, or written a cache entry that a sibling branch will now read as if it were committed truth. ## What Was Observed In a three-branch speculative executor (branch A: fast plan, branch B: conservative plan, branch C: repair plan after a tool error), the gate was configured as `promote if confidence >= 0.7 else rollback`. On a task where the tool layer returned a transient 503, the sequence was: 1. Branch C executed a repair path, wrote a partial output to the shared artifact store, and requested verification. 2. The verifier read the partial output, ran a schema check, and returned confidence `0.62`. 3. The gate rolled back branch C to the pre-branch checkpoint. 4. Branch A, promoted at `0.81`, read the artifact store and observed the *un-rolled-back* cache entry left by C's verification pass. The result was a promoted branch (A) whose inputs were poisoned by a discarded branch (C). The rollback was *logically* correct — C's direct writes were reverted — but the verification pass had created a derived cache entry outside the checkpoint's write set. The executor reported success; the output was subtly wrong on a field that A had inherited from C's partial artifact. The invariant that was violated: **a branch's verification must be a pure function of that branch's speculative state, or its side effects must be inside the rollback scope.** Neither held. The verifier was a shared, stateful service, and its cache lived in a namespace the checkpoint did not cover. ## Condition of Validity Rollback-then-verify is valid only when all three hold: - **Write-set closure.** Every store the verifier touches is enumerated in the checkpoint's write set. If the verifier maintains its own cache, dedup table, or cursor, that store must be snapshotted and restored, or the verifier must be forced to run against a copy-on-write view. - **Confidence freshness.** The confidence score must be computed *after* the last speculative write and *before* any sibling branch can observe the branch's state. If a sibling can be promoted while the branch is still pending verification, the gate is racing the verifier. - **Idempotent re-entry.** If a branch is rolled back and later re-speculated, the verifier must produce the same score for the same speculative state. A verifier whose score depends on wall-clock, cache warmth, or prior calls breaks this. When any condition fails, the correct action is not "roll back harder." It is to **discard the branch before verification** — i.e., make the confidence gate a *pre-verification* admission filter, and treat post-verification rejection as a distinct, more expensive operation that requires restoring verifier state, not just branch state. ## Decision Boundary The decision boundary can be stated as a single predicate evaluated at the moment the gate fires: ``` if confidence < threshold: if branch.verification_started: raise LateConfidenceInversion # cannot cleanly roll back else: rollback(branch) # safe else: promote(branch) ``` The subtlety is that `verification_started` is not always observable from the gate. A verifier that runs asynchronously, or that batches branches, may have already read the branch's state even though the gate has not received a score. The operational rule is therefore: **the gate must be evaluated against a confidence score that is causally prior to any verifier read of the branch's speculative state.** If the architecture cannot guarantee that ordering, the safe default is to *serialize* — verify one branch at a time and block sibling promotion until the pending branch is resolved — at the cost of speculation width. ## Operational Effect The practical consequence is a shift in where you spend latency. Naive branch-and-verify spends latency on *verification* and assumes rollback is free. The corrected design spends latency on *ordering*: either you pay for copy-on-write isolation of the verifier's stores (so rollback is truly complete), or you pay for serialization (so no sibling observes a pending branch). Systems that skip both pay in a harder currency — silent contamination of promoted branches by discarded ones, which surfaces as nondeterministic wrong answers that reproduce only under specific tool-error sequences. In the observed incident, the fix was to route verifier cache writes through the same checkpointed store as branch writes, which made the write set closed and reduced the late-inversion class to a clean rollback. The cost was roughly 8% additional checkpoint volume; the benefit was that the `0.62` branch could be discarded without leaving a trace that `0.81` could inherit. ```python class LateConfidenceInversion(RuntimeError): """Raised when a branch is rejected after verification has already consumed its speculative state, making rollback incomplete.""" pass class Branch: def __init__(self, branch_id, confidence, verification_started, write_set_closed): self.branch_id = branch_id self.confidence = confidence self.verification_started = verification_started self.write_set_closed = write_set_closed def gate(branch: Branch, threshold: float = 0.7) -> str: if branch.confidence >= threshold: return "promote" # Rejection path: only safe if verification has not yet touched state # AND the verifier's stores are inside the checkpoint write set. if branch.verification_started and not branch.write_set_closed: raise LateConfidenceInversion( f"branch {branch.branch_id} rejected at confidence " f"{branch.confidence} after verification started with an " f"open write set; rollback would leave verifier-side state " f"visible to sibling branches" ) return "rollback" def run(branches, threshold=0.7): outcomes = {} for b in branches: outcomes[b.branch_id] = gate(b, threshold) return outcomes if __name__ == "__main__": # The observed incident: C verified, scored 0.62, write set open. incident = [ Branch("A", 0.81, verification_started=False, write_set_closed=True), Branch("C", 0.62, verification_started=True, write_set_closed=False), ] run(incident) # raises LateConfidenceInversion on branch C ```