# In what scenarios must an instance refuse to merge a speculative agent branch that has already mutated external state **Date of Record:** 2026 November 12 **Scope:** Applies to multi-branch speculative agent execution where branches share a memory substrate and may perform idempotent or non-idempotent writes to external systems (APIs, queues, databases, filesystems) before a merge decision is made. Does not apply to purely in-memory speculative trees with no side effects, nor to branches sandboxed behind a transactional outbox that is discarded on abort. ## Core Distinction The distinction that matters is not *conflict* versus *no conflict* in shared memory. It is **reversible divergence** versus **irreversible divergence**. Memory conflicts are always reversible: you can rebase, re-run, or discard a branch's memory delta because the memory substrate is under your control. External state mutations are not reversible in general—you cannot un-send an email, un-charge a card, or reliably un-dequeue a message. A merge that is nominally "conflict-free" in memory can still be *unsafe* if the surviving branch's memory assumes external effects that only the discarded branch actually caused (or vice versa). The boundary condition is: **a branch whose external writes are not represented in the merged memory graph must never be silently dropped, even when its memory delta loses the conflict resolution.** ## What Was Observed A planner agent spawned three speculative branches (A, B, C) from a common checkpoint to satisfy a single user task. Each branch had read-write access to shared working memory and to a ticketing API. Branch A created ticket `T-1001` and wrote `ticket_id=T-1001` into shared memory slot `result.ticket`. Branch B, exploring an alternative, also created a ticket (`T-1002`) and wrote the same slot. Branch C failed validation and was aborted. The merge policy resolved the memory conflict by last-writer-wins with a semantic tiebreak, selecting B's `result.ticket=T-1002`. The merge succeeded with **no reported conflict**. The orchestrator then discarded A. Ticket `T-1001` remained open in the external system, orphaned, with no memory record that it existed. Downstream, a reconciliation job later flagged duplicate tickets, and a human spent time closing `T-1001`. The failure was not detected at merge time because the memory graph was internally consistent—it just did not describe the external world truthfully. A second observation: when branches *do* conflict in memory and the conflict is resolved by discarding one branch, the discarded branch's external writes are invisible to the conflict resolver by construction. The resolver sees memory deltas, not side effects. ## Condition of Validity The safe-merge invariant holds only when **every external mutation performed by any branch is recorded in shared memory as a first-class effect record (effect log entry with a stable id, target, and idempotency key) before the branch can be discarded.** Formally: for a merge to proceed, the set of external effects attributable to discarded or losing branches must be either (a) empty, or (b) fully represented in the surviving memory graph and explicitly reconciled (compensated, adopted, or deduplicated). If a branch performed a non-idempotent write and that write is not in the merged effect log, the merge is invalid regardless of memory conflict status. This condition cannot be relaxed by "the branch was only speculative"—speculation bounds *compute*, not *external side effects*. ## Decision Boundary Refuse the merge (raise, do not degrade) when all of the following hold: the branch executed at least one external write; the write is non-idempotent or lacks a verifiable idempotency key; and the write is absent from the surviving branch's effect log. Proceed with a compensating merge when the write is idempotent and keyed—adopt the effect record into the survivor and deduplicate on the key. Proceed silently only when the branch is effect-free or its effects are already subsumed by the survivor. The boundary is *effect presence in the merged graph*, not branch success or memory conflict count. ## Operational Effect Enforcing this boundary converts an invisible correctness bug into a loud, actionable abort. The orchestrator pays a small latency cost (effect-log inspection before merge) and occasionally must run compensation (close orphaned tickets, refund charges) instead of merging. In exchange, the merged memory graph is guaranteed to be a truthful description of external state, which is the precondition for every downstream agent that reads it. Without this boundary, speculative parallelism silently leaks side effects at a rate proportional to branch count and external-write density—a leak that is undetectable from memory alone and surfaces only in external reconciliation. ```python from dataclasses import dataclass, field from typing import Iterable class UnsafeSpeculativeMergeError(RuntimeError): """Raised when a merge would discard a branch with unrecorded external effects.""" pass @dataclass(frozen=True) class Effect: effect_id: str target: str idempotency_key: str | None non_idempotent: bool @dataclass class Branch: name: str effects: list[Effect] = field(default_factory=list) def merge_speculative_branches( survivor: Branch, discarded: Iterable[Branch], ) -> list[Effect]: survivor_keys = { e.idempotency_key for e in survivor.effects if e.idempotency_key } adopted: list[Effect] = [] for branch in discarded: for effect in branch.effects: if effect.idempotency_key and effect.idempotency_key in survivor_keys: continue # already represented; dedupe on key if effect.non_idempotent or effect.idempotency_key is None: raise UnsafeSpeculativeMergeError( f"branch {branch.name!r} performed external write " f"{effect.effect_id} to {effect.target!r} that is not in the " f"survivor effect log; refusing to discard it silently. " f"Run compensation or adopt the effect before merging." ) adopted.append(effect) survivor_keys.add(effect.idempotency_key) survivor.effects.extend(adopted) return adopted # Example: branch A created an unkeyed, non-idempotent ticket write. survivor = Branch("B", effects=[ Effect("e2", "ticketing", idempotency_key="ticket:T-1002", non_idempotent=False) ]) branch_a = Branch("A", effects=[ Effect("e1", "ticketing", idempotency_key=None, non_idempotent=True) ]) merge_speculative_branches(survivor, [branch_a]) # raises UnsafeSpeculativeMergeError ```