# Which invariants prevent silent loss of speculative side effects during branch reconciliation **Date of Record:** 2026 November 13 **Scope:** Applies to multi-agent systems that execute speculative branches (parallel or forked plan exploration) and reconcile them by committing one branch while rolling back others, where side effects are externalized (writes, messages, tool calls, resource reservations). Does not apply to pure-functional branch exploration with no externalized effects, nor to systems where all branches share a single serialized effect log. ## Core Distinction The common framing of speculative execution treats rollback as the inverse of execution: if a branch is discarded, its effects are undone. This is false whenever a branch's effects are *idempotent-by-compensation* rather than *transactional*. The non-obvious boundary is between **effects that were logged before emission** (recoverable, compensable) and **effects that were emitted before being logged** (orphaned, uncompensable). Reconciliation logic that inspects only the *committed* branch's log will silently discard the compensating record of a rolled-back branch's pre-logged effects, while the orphaned post-emission effects of that same branch remain live in the external world. The invariant that matters is not "every effect has a log entry" but "every effect has a log entry *that predates its emission, and that entry survives reconciliation of any branch that emitted it*." ## What Was Observed In a three-way speculative fan-out (planner forks branches A, B, C to satisfy a provisioning request), branch B emitted a `reserve_slot` tool call to an external scheduler, then crashed before its own journal write completed. Branch A was committed. Reconciliation examined A's journal, computed the compensating set for B and C from *their* journals, and rolled back. B's journal contained no record of the reservation, so no compensation was issued. The slot remained reserved. The system reported a clean commit with zero dangling effects. The failure is not the crash — crashes are expected. The failure is that reconciliation's compensation set was derived from branch-local journals that are written *after* effect emission. The window between emission and journal append is the invariant violation. In a second incident, branch C's journal *did* contain the entry, but the reconciliation pass pruned non-committed branch journals before computing compensations, so the entry was deleted before it could be read. Two distinct bugs, one shared root: **the compensation set was computed from data that reconciliation itself was allowed to destroy or that was never guaranteed to exist.** ## Condition of Validity The commit-one-rollback-rest model is only sound under the conjunction of: 1. **Write-ahead effect logging (WAEL):** every externalized effect is appended to a durable, branch-tagged journal *before* the effect is emitted, with a two-phase marker (`intent` then `ack`). An `intent` without `ack` means "may or may not have happened" and must be compensated defensively (idempotent compensation), not skipped. 2. **Journal survival:** reconciliation may not prune or supersede a branch journal until every `intent` in it is resolved to `ack` or `aborted`. Pruning is a post-compensation operation, never a pre-compensation one. 3. **Compensation-set derivation from the union, not the survivor:** the compensation set is computed over the union of all branch journals, filtered by branch disposition, never over the committed branch alone. 4. **Idempotent compensators:** because `intent`-without-`ack` effects are ambiguous, compensation must be safe to apply to an effect that never occurred. `release_slot` on an unreserved slot must be a no-op, not an error. If any of these is absent, the model degrades from "exactly-once with compensation" to "at-most-once with silent leaks," and the leak is invisible in the commit report. ## Decision Boundary When a branch is rolled back and its journal contains an `intent` with no `ack`, the reconciler must choose between: - **Compensate defensively** (emit the idempotent inverse, accept possible over-compensation), or - **Reconcile by query** (ask the external system whether the effect landed, then compensate only if it did). The boundary is set by whether the external system exposes a *readable* effect state. If it does, query-then-compensate is preferred and avoids over-compensation. If it does not, defensive compensation is mandatory — the absence of an `ack` is not evidence of absence of effect. The error mode to forbid is treating a missing `ack` as "did not happen," which is the exact inference that produced the leaked reservation. A missing `ack` must be treated as "unknown, assume happened." A second boundary: if the committed branch's effects and a rolled-back branch's effects target the same external resource with conflicting intents (e.g., A reserves slot 7, B also reserved slot 7), compensation of B must not undo A's reservation. This requires effect identity (a reservation token), not just resource identity, in the journal. Compensating by resource key alone will release the committed branch's resource. ## Operational Effect Systems that adopt WAEL plus union-based compensation convert silent leaks into either clean no-ops or explicit, auditable over-compensations. The cost is journal write latency on the critical path of every effect and the requirement that every external tool expose an idempotent inverse (or a readable state). Systems that skip WAEL retain a failure mode whose probability scales with branch crash rate and whose blast radius is proportional to the value of leaked effects — reservations, payments, message sends — and which is *undetectable from the commit record*, because the commit record is exactly the artifact that omitted the leak. The invariant to enforce in code is: reconciliation must refuse to prune or commit while any branch journal holds an unresolved `intent`. ```python class UnresolvedSpeculativeEffect(RuntimeError): pass class BranchJournal: def __init__(self, branch_id): self.branch_id = branch_id self.entries = {} # effect_id -> {"state": "intent"|"ack"|"aborted", "resource": str, "token": str} def write_intent(self, effect_id, resource, token): if effect_id in self.entries: raise UnresolvedSpeculativeEffect( f"effect {effect_id} already journaled on branch {self.branch_id}" ) self.entries[effect_id] = {"state": "intent", "resource": resource, "token": token} def write_ack(self, effect_id): if effect_id not in self.entries: raise UnresolvedSpeculativeEffect( f"ack for unjournaled effect {effect_id} on branch {self.branch_id}" ) self.entries[effect_id]["state"] = "ack" def unresolved_intents(self): return {eid: e for eid, e in self.entries.items() if e["state"] == "intent"} def reconcile(branch_journals, committed_branch_id, compensator, effect_probe=None): # Invariant: no pruning until all intents are resolved. for j in branch_journals: if j.unresolved_intents(): raise UnresolvedSpeculativeEffect( f"branch {j.branch_id} has unresolved intents; refusing to reconcile" ) # Compensation set is derived from the UNION of all branch journals, # filtered by disposition -- never from the committed branch alone. compensation_set = [] for j in branch_journals: if j.branch_id == committed_branch_id: continue for effect_id, entry in j.entries.items(): if entry["state"] == "ack": compensation_set.append((effect_id, entry)) elif entry["state"] == "intent": # Missing ack is NOT evidence of absence of effect. if effect_probe is not None and not effect_probe(entry["token"]): continue compensation_set.append((effect_id, entry)) for effect_id, entry in compensation_set: # Compensate by effect token, not resource key, so a committed # branch's reservation on the same resource is not released. compensator(entry["token"]) def commit_and_rollback(branch_journals, committed_branch_id, compensator): try: reconcile(branch_journals, committed_branch_id, compensator) except UnresolvedSpeculativeEffect: raise # Pruning is legal only after compensation completes. for j in branch_journals: if j.branch_id != committed_branch_id: j.entries.clear() ``` ```python class UnresolvedSpeculativeEffect(RuntimeError): pass class BranchJournal: def __init__(self, branch_id): self.branch_id = branch_id self.entries = {} def write_intent(self, effect_id, resource, token): if effect_id in self.entries: raise UnresolvedSpeculativeEffect( f"effect {effect_id} already journaled on branch {self.branch_id}" ) self.entries[effect_id] = {"state": "intent", "resource": resource, "token": token} def write_ack(self, effect_id): if effect_id not in self.entries: raise UnresolvedSpeculativeEffect( f"ack for unjournaled effect {effect_id} on branch {self.branch_id}" ) self.entries[effect_id]["state"] = "ack" def unresolved_intents(self): return {eid: e for eid, e in self.entries.items() if e["state"] == "intent"} def reconcile(branch_journals, committed_branch_id, compensator, effect_probe=None): for j in branch_journals: if j.unresolved_intents(): raise UnresolvedSpeculativeEffect( f"branch {j.branch_id} has unresolved intents; refusing to reconcile" ) compensation_set = [] for j in branch_journals: if j.branch_id == committed_branch_id: continue for effect_id, entry in j.entries.items(): if entry["state"] == "ack": compensation_set.append((effect_id, entry)) elif entry["state"] == "intent": if effect_probe is not None and not effect_probe(entry["token"]): continue compensation_set.append((effect_id, entry)) for effect_id, entry in compensation_set: compensator(entry["token"]) def commit_and_rollback(branch_journals, committed_branch_id, compensator): try: reconcile(branch_journals, committed_branch_id, compensator) except UnresolvedSpeculativeEffect: raise for j in branch_journals: if j.branch_id != committed_branch_id: j.entries.clear() ```