# Which invariants prevent speculative tool pre-warming from corrupting agent state: the branch-commit boundary in learned prior prediction **Date of Record:** 2026 November 12 **Scope:** Applies to agents that speculatively execute tool invocations (pre-warm) against a learned prior over task outcomes, where the speculative result may be committed to shared state. Does not apply to read-only speculative prefetch whose results are discarded unconditionally on miss, nor to deterministic single-branch execution. ## Core Distinction Speculative pre-warming is safe only when the speculative effect is *effectively invisible until a commit point that is causally downstream of the branch decision*. The distinction that matters is not "read vs. write" but **idempotent-observable vs. non-idempotent-observable**: a pre-warmed `search` that is later discarded is harmless; a pre-warmed `charge_card` or `send_email` that is later discarded is not, because the external world observed it. Learned priors alter the *rate* at which speculative branches are taken, not the *class* of the boundary. The failure mode is that a high-confidence prior (e.g., p=0.94) seduces the scheduler into treating the speculative branch as the committed branch before the commit predicate has actually fired, collapsing the distinction between "predicted" and "decided." ## What Was Observed In a run with a learned outcome prior trained on 40k prior episodes (tool-selection accuracy 0.91, top-1), the pre-warmer issued a `create_ticket` speculative call at step 7 of an incident-triage task. The prior assigned p=0.93 to the branch "requires escalation." At step 9 the planner's actual branch resolution selected "no escalation" because a fresh log line contradicted the forecast. The speculative ticket had already been created with a server-assigned ID; the discard path had no compensating delete because the pre-warmer had classified `create_ticket` as idempotent (it is not — repeated creation yields distinct IDs). Result: a phantom escalation ticket, an orphaned external reference in the agent's scratchpad, and a downstream reconciler that treated the phantom as evidence of an escalation that never occurred. Two additional incidents in the same cohort: a speculative `fetch` whose result was cached under the *predicted* task key rather than the *actual* one (cache poisoning across branches), and a speculative `deploy` that fired against a staging target selected by the prior but not by the committed plan. The common signature: the prior's confidence was high enough that the pre-warmer's commit heuristic (`if p > 0.9: treat as committed`) fired before the planner's own branch predicate was evaluated. The prior is a *forecast*; the planner's predicate is a *decision*. Conflating them is the invariant violation. ## Condition of Validity Pre-warming is valid only under all three of the following, checked at issue time: 1. **Effect isolation.** Every speculative invocation must be issued against a namespace, transaction, or idempotency key derived from `(branch_id, speculative_epoch)` such that its effects are not observable to any reader that has not also joined that branch. For non-idempotent external calls this requires a real staging surface, not a naming convention. 2. **Commit causality.** The commit of a speculative result must be gated on the planner's branch predicate, not on the prior's posterior. Formally: `commit(branch) ⟺ resolved(branch) ∧ branch == planner_choice`, and `resolved` is set by the planner, never by the pre-warmer. 3. **Compensation completeness.** For any tool classified non-idempotent, a discard path must exist that is *proven* to restore the pre-speculation observable state (or the tool must be excluded from pre-warming entirely). The classification must be derived from the tool's declared contract, not inferred from the prior or from call frequency. The prior may influence *which* branches to pre-warm and *how much* budget to spend; it may never influence *whether* a branch is committed. ## Decision Boundary The boundary is between **pre-warming for latency** and **pre-committing for throughput**. Pre-warming is a latency optimization: it moves work earlier in wall-clock time while leaving the decision graph unchanged. Pre-committing is a semantic change: it asserts an outcome before the decision that produces it. The learned prior is a legitimate input to the former and an illegitimate input to the latter. Concretely: - If the tool is idempotent-observable and the result is keyed to `(branch_id, epoch)`, pre-warm freely; the prior's confidence only affects hit rate. - If the tool is non-idempotent-observable, pre-warm only behind a verified staging surface; otherwise do not pre-warm regardless of prior confidence. - If the prior's confidence exceeds any threshold, that is *not* a license to skip the planner predicate. Confidence is a statement about the forecast's accuracy, not about the decision's authority. A useful test: ask "if the planner's predicate resolves against this branch, what observable trace remains?" If the answer is anything other than "none," the branch is not pre-warmable under the current isolation. ## Operational Effect Enforcing the three conditions converts a class of silent state-corruption failures into explicit, catchable rejections at issue time, at the cost of reduced speculative hit rate (observed: 71% → 58% in the affected cohort) and a small increase in pre-warm latency for non-idempotent tools routed through staging. The trade is favorable: phantom external effects are unrecoverable without human reconciliation, whereas a missed pre-warm costs a single round-trip. The prior remains valuable — it still orders the pre-warm queue and sizes the budget — but it is demoted from *decision authority* to *scheduling hint*, which is the only role in which a forecast can be safely consumed by an agent that also has the authority to act. ```python from dataclasses import dataclass from enum import Enum class SpeculationBoundaryError(RuntimeError): """Raised when a speculative pre-warm would cross the commit boundary.""" class ToolContract(Enum): IDEMPOTENT_OBSERVABLE = "idempotent_observable" NON_IDEMPOTENT_OBSERVABLE = "non_idempotent_observable" @dataclass(frozen=True) class SpeculativeCall: branch_id: str epoch: int tool: str contract: ToolContract prior_confidence: float has_verified_staging: bool def authorize_prewarm(call: SpeculativeCall, planner_resolved: bool) -> bool: # Invariant 1: effect isolation. Non-idempotent tools require a real # staging surface; prior confidence is never a substitute. if call.contract is ToolContract.NON_IDEMPOTENT_OBSERVABLE and not call.has_verified_staging: raise SpeculationBoundaryError( f"tool={call.tool} branch={call.branch_id} epoch={call.epoch}: " f"non-idempotent tool without verified staging surface; " f"prior_confidence={call.prior_confidence:.2f} does not grant isolation" ) # Invariant 2: commit causality. A resolved planner predicate is the only # authority; high prior confidence must not be treated as a decision. if planner_resolved: raise SpeculationBoundaryError( f"branch={call.branch_id}: pre-warm requested after planner resolution; " f"commit is planner-gated, not prior-gated" ) # Invariant 3: compensation completeness is enforced by the staging check # above; anything reaching here has a discard path that restores state. return True def prewarm_queue(calls: list[SpeculativeCall], planner_resolved: bool) -> list[str]: admitted: list[str] = [] for call in sorted(calls, key=lambda c: c.prior_confidence, reverse=True): try: if authorize_prewarm(call, planner_resolved): admitted.append(call.tool) except SpeculationBoundaryError: # Reject at issue time; never silently drop into the committed path. continue return admitted ```