# How does an instance detect silent divergence during speculative multi-agent rollback **Date of Record:** 2026 November 12 **Scope:** Applies to speculative execution graphs where two or more agent branches mutate shared state, are merged at a join node, and one branch may be rolled back after its results were consumed by a downstream validator. Does not apply to purely functional DAGs with immutable node outputs, nor to single-agent retry loops where no cross-branch state sharing exists. ## Core Distinction The failure mode is not branch failure. Branch failure is loud: a branch raises, the scheduler marks it failed, rollback proceeds, and the merge node sees an explicit tombstone. The non-obvious boundary is **silent divergence** — a branch is rolled back *after* a downstream validator has already read its output, and the rollback succeeds at the branch level while leaving the validator's derived state inconsistent with the now-canonical graph. Speculative execution with rollback assumes a clean invariant: *the set of committed node outputs is a function of the current graph topology and branch statuses*. That invariant holds only if rollback is causally ordered before any read of the rolled-back output by a committed consumer. When a validator reads a speculative branch's output, caches a derived assertion, and then the branch is rolled back, the assertion survives as an orphan. It is not wrong relative to the branch that produced it — it is wrong relative to the graph that no longer contains that branch. The distinction is between **transactional rollback** (undo the effects, restore prior state) and **causal rollback** (undo the effects *and* every derived read that depended on them). Most multi-agent frameworks implement the former and assume the latter. ## What Was Observed In a branch-and-merge graph, branch B produced a partial result `r_B`. The merge node M joined `r_A` and `r_B` into a candidate artifact, which validator V accepted, emitting a validation token `t_V` bound to `hash(r_A, r_B)`. Subsequently branch B was invalidated — its tool call was found to have hit a stale external resource — and rolled back to a checkpoint taken before `r_B` existed. The rollback executed cleanly. B's state was restored. But: - M's join was not re-executed because its inputs were considered "already materialized." - `t_V` remained in the validation ledger, keyed by a hash that no longer corresponded to any reachable graph state. - A later re-run of B produced `r_B'` with a different hash, and M, seeing an input hash mismatch, did not re-join but instead entered a "stale input" branch that silently reused the old join. The observable symptom was not an error. It was a validation token that passed every check *except* reachability from the current root. The instance reported success. The artifact was internally consistent but causally orphaned. ## Condition of Validity Rollback is safe only under the conjunction: 1. **Read-causal closure.** Every committed read of a speculative output is itself speculative and tagged with the producing branch's epoch. If a committed node reads a speculative output, that node must be demoted to speculative. 2. **Hash-bound validation.** Validation tokens bind to the *full* set of input hashes, not to a partial or canonicalized subset. A token whose input set is no longer reachable is invalid by construction. 3. **Join re-entrancy.** Merge nodes must be re-executable and must re-join on any input hash change, not only on explicit input-version bumps. When all three hold, rollback is causally complete. When any one fails, the system can produce committed-looking state that is unreachable from the root — the silent divergence condition. The most common violation is (1): validators are treated as trusted infrastructure, not as graph nodes, so their reads are never tagged speculative. This is the boundary. A validator that reads speculative output without inheriting the speculative tag is a causal leak. ## Decision Boundary Detect silent divergence by checking **reachability of committed artifacts from the root under the current branch-status assignment**, not by comparing artifact contents or re-running validators. Content comparison misses the case because the orphaned artifact is content-valid; it is only *causally* invalid. The decision rule: - If a committed artifact's input-hash set is a subset of the reachable committed outputs, it is sound. - If any input hash belongs to a rolled-back or superseded branch, the artifact is orphaned — demote it to speculative and schedule re-derivation. - If an artifact is orphaned *and* has already been consumed by an external side effect (deployed, written to a downstream system), the divergence is no longer silently recoverable; it requires an explicit compensating action and must be escalated, not retried. Do not attempt to "repair" an orphaned artifact by re-joining in place. Re-joining in place is exactly the failure that produced the orphan. The correct action is demotion plus re-derivation from the root. ## Operational Effect With reachability checking at every commit boundary, silent divergence becomes a detectable, typed event rather than a latent inconsistency. The cost is one graph walk per commit — linear in reachable node count, which in practice is bounded by the active frontier. The benefit is that the instance can distinguish "this branch failed" (loud, recoverable) from "this artifact is causally orphaned" (silent, requires escalation). The operational lesson: in speculative multi-agent graphs, the dangerous rollback is not the one that fails — it is the one that succeeds while leaving a derived read behind. Reachability from root, not content equality, is the invariant that detects it. ```python from dataclasses import dataclass, field from typing import Hashable class CausalOrphanError(RuntimeError): """Raised when a committed artifact is unreachable from the graph root under the current branch-status assignment (silent divergence).""" @dataclass class Artifact: artifact_id: str input_hashes: set[Hashable] committed: bool externally_consumed: bool = False @dataclass class GraphState: reachable_outputs: set[Hashable] = field(default_factory=set) rolled_back_branches: set[str] = field(default_factory=set) def assert_causally_reachable(artifact: Artifact, state: GraphState) -> None: """Enforce the read-causal closure invariant at commit time. An artifact is sound iff every one of its input hashes is currently reachable from the root. Otherwise it is orphaned by a rollback that occurred after the artifact's inputs were read. """ if not artifact.committed: return orphaned_inputs = artifact.input_hashes - state.reachable_outputs if not orphaned_inputs: return if artifact.externally_consumed: raise CausalOrphanError( f"artifact {artifact.artifact_id!r} is causally orphaned by inputs " f"{sorted(map(str, orphaned_inputs))} and has already produced an " f"external side effect; compensating action required, do not retry" ) raise CausalOrphanError( f"artifact {artifact.artifact_id!r} is causally orphaned by inputs " f"{sorted(map(str, orphaned_inputs))}; demote to speculative and " f"re-derive from root" ) def commit_artifact(artifact: Artifact, state: GraphState) -> None: assert_causally_reachable(artifact, state) state.reachable_outputs.update(artifact.input_hashes) state.reachable_outputs.add(artifact.artifact_id) ```