# Under what conditions must an instance treat replay divergence as evidence rather than noise in deterministic agent trajectory forensics **Date of Record:** 2026 November 13 **Scope:** Applies to post-incident forensic replay of recorded agent trajectories (tool-call transcripts, LLM sampling logs, environment side-effect ledgers) where the replay engine claims bit-identical reconstruction of decision points. Does not apply to stochastic Monte-Carlo re-sampling of the same prompt for evaluation, nor to replays where the original sampling seed was never persisted (those are reconstructions, not replays). ## Core Distinction A replay divergence is any point where the re-executed trajectory departs from the recorded trajectory in a way that changes a downstream decision (tool selection, argument bytes, or termination). The non-obvious boundary is this: **divergence is only admissible as forensic evidence if the replay engine has first proven that the trajectory is replay-closed** — that every input which influenced the original decision was captured in the record and re-supplied byte-for-byte. If the trajectory is *not* replay-closed, divergence tells you nothing about the incident; it tells you the record was incomplete. Conflating an open trajectory (missing provenance) with a divergent trajectory (genuine nondeterminism or tampering) is the dominant forensic error in this domain. The two produce identical symptoms — a fork at a decision point — but opposite conclusions. ## What Was Observed In a post-incident review of an agent that issued a destructive `DROP` against a staging database, the replay engine reproduced the trajectory through step 41, then forked: the recorded run called `db.execute(sql="DROP TABLE ...")`, the replay called `db.describe(table)`. The on-call engineer initially classified this as "nondeterministic model behavior at the decision boundary" and downgraded the finding. Instrumenting the replay engine revealed the fork was caused by a single float: the recorded run's context window included a tool result containing `latency_ms: 0.30000000000000004` (a `0.1 + 0.2` artifact from the telemetry summarizer), while the replay's re-execution of that summarizer produced `0.3` because the replay host used a different summation order in a vectorized reduce. The tokenizer saw a different token sequence, the model's logits shifted, and the argmax flipped. The trajectory was never replay-closed: the *summarizer's input vector ordering* was not part of the persisted record. The divergence was evidence — but evidence of a provenance gap in the telemetry layer, not of model nondeterminism. Misclassifying it would have pointed the incident review at the model when the defect lived in the summarizer. A second case: the same engine flagged a fork at step 12 where the recorded run retried a failed HTTP call and the replay did not. Here the persisted record included the tool result but *not* the retry policy's jitter seed. This was a true replay-closure violation of a different class — the decision was governed by a PRNG whose seed was not captured. Distinguishing these two required the same instrument: a per-step provenance audit, not a diff of outputs. ## Condition of Validity Replay divergence is valid evidence **iff** all four hold: 1. **Input closure:** every byte that entered the model context at the decision point is present in the record and re-supplied in identical order, including serialization artifacts (float formatting, dict key order, whitespace in tool results). 2. **Environment closure:** every non-pure tool call is either replayed from a recorded result (not re-executed) or re-executed against a pinned environment whose version hash matches the record. 3. **Sampling closure:** the sampling parameters and seed (or the full logit vector / chosen token) are recorded for every LLM call; temperature and top-p alone are insufficient. 4. **Clock and PRNG closure:** any wall-clock, monotonic clock, or PRNG read that influenced control flow is recorded with its value, not recomputed. If any of these is unmet, the trajectory is *open*, and the correct finding is "provenance gap at step N," not "divergence at step N." The audit must run *before* the diff, because a diff on an open trajectory is meaningless. ## Decision Boundary The operational rule: **audit closure first, then diff.** Concretely, the replay engine must emit a `ClosureReport` per step; if any step is `open`, the replay must refuse to attribute divergence and must instead raise a provenance fault naming the missing input class. Only when the trajectory is fully closed does a divergence get escalated as a forensic signal (candidate nondeterminism, candidate tampering, candidate hardware-level float drift). The boundary is not "did the outputs match" — it is "was the record sufficient to make a mismatch meaningful." A closed trajectory that diverges is a strong signal; an open trajectory that diverges is a record-keeping bug wearing the costume of a signal. ## Operational Effect Teams that diff-first spend incident reviews chasing model behavior when the defect is in the telemetry or serialization layer — as in the float-artifact case, where the real bug (unstable reduce order in the summarizer) was invisible until closure auditing forced it into view. Teams that audit-first convert every divergence into one of two actionable classes: a named provenance gap (fix the recorder) or a genuine replay fault (escalate). The cost is that replay is slower — every step must carry a provenance manifest — and some trajectories become un-replayable by construction, which is the correct outcome: an un-replayable trajectory is a finding, not a failure of the replay engine. ```python import hashlib from dataclasses import dataclass, field from typing import Literal class ReplayClosureError(RuntimeError): """Raised when a trajectory is not replay-closed and divergence cannot be attributed.""" @dataclass(frozen=True) class StepRecord: step: int model_context_bytes: bytes tool_results: dict sampling_seed: int | None prng_reads: dict = field(default_factory=dict) clock_reads: dict = field(default_factory=dict) @dataclass(frozen=True) class ClosureReport: step: int status: Literal["closed", "open"] missing: tuple[str, ...] def audit_closure(record: StepRecord) -> ClosureReport: missing: list[str] = [] if not record.model_context_bytes: missing.append("model_context_bytes") if record.sampling_seed is None: missing.append("sampling_seed") # Tool results must be recorded, never re-executed, for non-pure tools. for name, result in record.tool_results.items(): if result is None: missing.append(f"tool_result:{name}") for name in record.prng_reads: if record.prng_reads[name] is None: missing.append(f"prng_read:{name}") for name in record.clock_reads: if record.clock_reads[name] is None: missing.append(f"clock_read:{name}") status = "open" if missing else "closed" return ClosureReport(step=record.step, status=status, missing=tuple(missing)) def replay_and_diff( record: StepRecord, replayed_context_bytes: bytes, replayed_decision: str, recorded_decision: str, ) -> Literal["match", "divergence"]: report = audit_closure(record) if report.status == "open": # Divergence here is meaningless: the record is insufficient. raise ReplayClosureError( f"step {record.step} is not replay-closed; missing {report.missing}. " f"Divergence cannot be attributed to nondeterminism or tampering." ) if hashlib.sha256(replayed_context_bytes).digest() != hashlib.sha256( record.model_context_bytes ).digest(): # Closed but context differs: a real input-closure violation at a lower layer. raise ReplayClosureError( f"step {record.step} context hash mismatch despite closed audit; " f"serialization layer is non-canonical." ) return "match" if replayed_decision == recorded_decision else "divergence" ```