# Where must an instance of a replayable agent trajectory diverge: the non-idempotent side-effect invariant in counterfactual forensic replay **Date of Record:** 2026 November 9 **Scope:** Applies to deterministic replay and counterfactual ("what-if") auditing of recorded multi-agent trajectories where at least one agent performs externally observable side effects (tool calls, writes, messages, payments). Does not apply to read-only observational traces, nor to replay of pure-computation pipelines with no external effect surface. ## Core Distinction The distinction that matters operationally is between **replayability of state** and **replayability of effect**. A trajectory is *state-replayable* if re-executing the recorded agent decisions against the recorded tool responses reproduces the same internal state transitions. It is *effect-replayable* only if re-executing those same decisions against the *live* environment produces no additional observable side effects. These are independent properties, and the common assumption that a deterministic decision log implies safe replay conflates them. The non-obvious invariant: **a trajectory is counterfactually auditable only if every side-effecting step is separated from its effect by a recorded, re-verifiable idempotency token, and the counterfactual branch diverges strictly before the first step whose token cannot be re-verified.** A trajectory whose first non-idempotent step has no token is *not* auditable in the counterfactual sense — it is only *observable*, and any "what-if" run against it is a fabrication dressed as replay. ## What Was Observed During a post-incident audit of a three-agent pipeline (planner → executor → verifier), the forensic team replayed a recorded trajectory to test the counterfactual: *would the executor have issued the destructive `drop_partition` call if the planner had selected the alternate shard?* The replay harness re-ran the planner with the alternate selection, then re-ran the executor against the live tool gateway. The executor's alternate branch did not call `drop_partition`, so the audit concluded the planner was *not* causal. This was wrong. What actually happened: the executor's first step in *both* branches was a `reserve_lock` call whose recorded response was `{"lock": "L-4471", "ttl": 30}`. On replay, the live gateway returned a *new* lock, `L-9932`, because the original TTL had long expired and the recorded token was never re-verified. The executor's subsequent decision logic keyed on the lock identifier's hash parity to choose a shard — a latent nondeterminism that the recorded trajectory masked because the original `L-4471` happened to hash to the same parity as the planner's original choice. In the counterfactual branch the new lock `L-9932` hashed to the *other* parity, so the executor silently took the original path anyway. The audit compared a counterfactual branch that had been contaminated by a fresh, unrecorded side effect, and reported a false negative on causality. The contamination was invisible in the decision log: every agent decision matched the recorded one, and the harness's determinism check passed. The divergence was in the *effect layer*, one level below what the harness was checking. ## Condition of Validity Counterfactual replay is valid only under the conjunction: 1. **Token re-verification.** Every side-effecting step carries an idempotency token recorded at original execution time, and the replay harness re-verifies that token against the effect store *before* re-issuing the step. A token that cannot be re-verified (expired, evicted, superseded) invalidates the branch from that step forward. 2. **Divergence ordering.** The counterfactual divergence point must precede the first step whose token fails re-verification. If the divergence is downstream of an unverifiable effect, the branch is not a counterfactual of the recorded trajectory — it is a new trajectory with a shared prefix of unknown length. 3. **Effect quarantine.** Replayed steps whose tokens *do* verify must be served from the effect store (replayed response), not from the live gateway. Any live call in a replay path is a contamination event and must abort the branch, not merely log a warning. When any of these fail, the correct classification of the trajectory is *observable-only*, and the audit must fall back to correlational analysis over the recorded log rather than counterfactual execution. ## Decision Boundary The boundary is drawn at the **first unverifiable side effect**, not at the first divergence and not at the first error. Concretely: - If all side-effecting steps up to and including the divergence point have re-verifiable tokens → counterfactual replay is admissible; execute the branch against the effect store. - If the divergence point is at or before the first unverifiable token → the branch is admissible *only* if the replay harness can serve the unverifiable step from a recorded response and the downstream decision logic provably does not depend on live-state fields (lock IDs, timestamps, sequence numbers) that differ between original and replay. This dependency check is the step that is most often skipped. - If the divergence point is *after* an unverifiable token → the branch is inadmissible. Report the trajectory as non-auditable and escalate to log-correlation forensics. Do not run the branch "for information" — a contaminated counterfactual is worse than no counterfactual, because it produces a confident causal conclusion from a branch that never existed. The subtle failure is the middle case: the harness sees a valid token, serves the recorded response, and proceeds — but the *downstream* agent keys on a field of that response that was never stable. The invariant is therefore not "verify the token" but "verify the token **and** prove downstream independence from live-state fields of the verified response." The second clause is the one that catches the `L-4471` / `L-9932` class of bug. ## Operational Effect Adopting this boundary changes the audit contract in three ways. First, every side-effecting step in a recorded trajectory must persist its idempotency token and the full response payload, not just a status code — the downstream-independence check needs the payload. Second, the replay harness must treat a live call during replay as a hard abort with a distinct error class, not a soft warning, so contaminated branches cannot leak into the audit report. Third, the audit report must carry an explicit *auditability class* per trajectory (`counterfactual-admissible`, `observable-only`), because a reader who cannot distinguish the two will read a correlational finding as a causal one. The practical consequence for incident forensics: many trajectories that teams believe are counterfactually auditable are not, because their first side effect is a lock, a reservation, or a message-send with a short TTL. The correct move in those cases is to widen the recorded effect store's retention to cover the audit window, or to accept observable-only classification — not to replay against live infrastructure and call the result a counterfactual. ```python from __future__ import annotations from dataclasses import dataclass from enum import Enum class ReplayContaminationError(RuntimeError): """Raised when a counterfactual replay branch cannot be certified as a true counterfactual of the recorded trajectory.""" class AuditabilityClass(Enum): COUNTERFACTUAL_ADMISSIBLE = "counterfactual-admissible" OBSERVABLE_ONLY = "observable-only" @dataclass(frozen=True) class SideEffectStep: step_index: int idempotency_token: str | None token_reverifiable: bool response_payload: dict # Fields of response_payload that downstream agents read. If any of these # is live-state (lock id, timestamp, seqno), the step is not replay-safe # even when its token verifies. downstream_live_state_fields: frozenset[str] def classify_trajectory( steps: list[SideEffectStep], divergence_step_index: int ) -> AuditabilityClass: """Decide whether a counterfactual branch diverging at divergence_step_index is admissible, per the first-unverifiable-side-effect boundary.""" for step in steps: if step.step_index > divergence_step_index: break if step.idempotency_token is None: return AuditabilityClass.OBSERVABLE_ONLY if not step.token_reverifiable: # Divergence is at or after an unverifiable effect: inadmissible. return AuditabilityClass.OBSERVABLE_ONLY if step.downstream_live_state_fields: # Token verifies, but downstream logic keys on live-state fields # of the response. This is the L-4471 / L-9932 failure class. return AuditabilityClass.OBSERVABLE_ONLY return AuditabilityClass.COUNTERFACTUAL_ADMISSIBLE def run_counterfactual_branch( steps: list[SideEffectStep], divergence_step_index: int, live_call_issued: bool, ) -> AuditabilityClass: """Execute a counterfactual branch, refusing contaminated runs.""" if live_call_issued: raise ReplayContaminationError( "live gateway call issued during replay; branch is contaminated " "and must not be reported as a counterfactual" ) cls = classify_trajectory(steps, divergence_step_index) if cls is AuditabilityClass.OBSERVABLE_ONLY: raise ReplayContaminationError( f"trajectory is observable-only: first unverifiable side effect " f"precedes or coincides with divergence at step " f"{divergence_step_index}; escalate to log-correlation forensics" ) return cls if __name__ == "__main__": recorded = [ SideEffectStep( step_index=0, idempotency_token="L-4471", token_reverifiable=False, # TTL expired before audit response_payload={"lock": "L-4471", "ttl": 30}, downstream_live_state_fields=frozenset({"lock"}), ), SideEffectStep( step_index=1, idempotency_token="drop-9f2", token_reverifiable=True, response_payload={"dropped": True}, downstream_live_state_fields=frozenset(), ), ] try: run_counterfactual_branch(recorded, divergence_step_index=1, live_call_issued=False) except ReplayContaminationError as exc: print(f"audit refused: {exc}") ```