# Under what conditions must an instance refuse to replay a tainted agent trace end-to-end **Date of Record:** 2026 November 8 **Scope:** Applies to post-incident taint reconstruction over recorded agent traces (tool calls, model outputs, retrieval reads, and inter-agent messages) where the replay engine is expected to reproduce the causal chain that caused a downstream side effect. Does not apply to pure observability replay (viewing spans without re-executing side-effecting tools) or to deterministic unit-level mock replay where no external state is touched. ## Core Distinction The non-obvious boundary is between **causal replay** and **stateful replay**. Causal replay reconstructs the *dependency graph* of reads and writes that produced a tainted output; it treats every tool invocation as an opaque node with recorded inputs/outputs. Stateful replay re-executes the tool against live or partially-live infrastructure to "confirm" the taint. These are not the same operation, and the invariant that matters is this: **a trace is replayable end-to-end only if every side-effecting node in the causal cone has a recorded, verifiable idempotency token and a terminal outcome (success, deterministic failure, or explicit abort).** If any node in the cone was recorded mid-flight — no terminal outcome, or a token that collided with a prior invocation — the replay engine must refuse the *entire* end-to-end path rather than reconstructing a plausible-looking but unverifiable chain. The failure is non-obvious because partial replay *succeeds* and produces a taint graph. The graph is simply wrong: it attributes a write to a read that never causally preceded it, or it silently drops a node that was retried under a reused token, collapsing two distinct effects into one. ## What Was Observed During a reconstruction of a data-exfiltration incident, a trace showed an agent issuing `read_secret` → `summarize` → `post_message`. The `post_message` node had no terminal outcome recorded: the process was killed between the outbound write and the acknowledgment. The replay engine, treating the missing outcome as "assume success," reconstructed a clean linear taint path and cleared the `summarize` node of suspicion. The actual incident involved a **retry under a reused idempotency token**: the first `post_message` had partially succeeded (message delivered to one of two sinks), the retry under the same token was deduplicated at the gateway, and the second sink received the payload via a separate path not represented in the trace. The end-to-end replay could not see the second sink because the token collision erased the second invocation from the record. Reconstructing the chain required treating the token as the unit of replay, not the span. A second observation: traces where a node's outcome was `aborted` but a downstream node still executed (because the abort was advisory, not enforced) produced a taint graph that *looked* causally consistent but violated the actual execution order. The replay engine must detect this — an executed successor of an aborted predecessor is a **causal violation**, and the replay must stop there, not paper over it. ## Condition of Validity End-to-end causal replay is valid if and only if, for every node `n` in the causal cone of the tainted output: 1. `n` has a terminal outcome recorded (`success`, `failure`, `abort`), not `pending` or `unknown`. 2. `n`'s idempotency token is unique within the trace's causal cone; no two distinct effects share a token. 3. Every successor of an `abort` node is itself `abort` or absent (no executed successor of an aborted predecessor). 4. The recorded input hash of `n` matches the input hash the replay engine would derive from its predecessors' outputs. If any condition fails, the trace is **partially replayable** at best. The engine must emit a `TaintReplayRefused` condition scoped to the cone, not a degraded graph. Degraded graphs are worse than no graph because they carry apparent authority. ## Decision Boundary The decision is not "replay or don't replay." It is **which unit of replay to adopt** when the span-level record is insufficient: - **Span-level replay** is valid when all four conditions above hold. Use it by default. - **Token-level replay** is required when a token collision is detected (condition 2 fails): reconstruct the effect chain from the idempotency ledger, not the span log, and attribute taint to every sink the token touched. - **Refusal** is required when a node has no terminal outcome and no ledger entry can resolve it (condition 1 fails irrecoverably). In that case the engine must refuse the end-to-end replay and report the *set of nodes* whose outcomes are unknowable, so a human or a higher-level agent can scope the blast radius manually. The boundary that trips operators: a trace with a single `unknown` node in the cone is not "mostly replayable." It is a trace whose taint attribution is undefined at that node, and any graph drawn through it is fabricated. ## Operational Effect Refusing end-to-end replay on a single unresolvable node reduces false-clean verdicts to zero at the cost of increasing manual scoping work. In the observed incident, the refusal would have flagged the `post_message` node immediately; the manual scoping then found the second sink in under an hour, versus the multi-day delay caused by trusting the degraded graph. The operational rule: **never let a replay engine emit a taint graph that contains a node with an unverified terminal outcome; emit the refusal and the node set instead.** Downstream consumers (incident responders, automated containment) must treat a refusal as a first-class signal, not an error to be retried. ```python from dataclasses import dataclass from enum import Enum from typing import Optional class Outcome(Enum): SUCCESS = "success" FAILURE = "failure" ABORT = "abort" PENDING = "pending" UNKNOWN = "unknown" class TaintReplayRefused(RuntimeError): """Raised when an end-to-end taint replay cannot be validly reconstructed.""" def __init__(self, node_id: str, reason: str, cone: list[str]): self.node_id = node_id self.reason = reason self.cone = cone super().__init__(f"replay refused at {node_id}: {reason} (cone={cone})") @dataclass class TraceNode: node_id: str outcome: Outcome idempotency_token: Optional[str] predecessor: Optional[str] = None predecessor_aborted: bool = False def validate_replayable(cone: list[TraceNode]) -> None: """Enforce the four replay-validity conditions over a causal cone.""" tokens: dict[str, str] = {} for node in cone: if node.outcome in (Outcome.PENDING, Outcome.UNKNOWN): raise TaintReplayRefused( node.node_id, f"non-terminal outcome {node.outcome.value}", [n.node_id for n in cone], ) if node.idempotency_token is None: raise TaintReplayRefused( node.node_id, "missing idempotency token on side-effecting node", [n.node_id for n in cone], ) if node.idempotency_token in tokens: raise TaintReplayRefused( node.node_id, f"idempotency token collision with {tokens[node.idempotency_token]}", [n.node_id for n in cone], ) tokens[node.idempotency_token] = node.node_id if node.predecessor_aborted and node.outcome == Outcome.SUCCESS: raise TaintReplayRefused( node.node_id, "executed successor of aborted predecessor (causal violation)", [n.node_id for n in cone], ) ```