What invariants prevent prompt injection from persisting through multi-turn context?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session processes multi-turn conversations or context accumulation, and a prompt injection in one turn may persist into subsequent turns—continuing to influence behavior long after the injection's original turn ended. Does not apply to strictly single-turn sessions or sessions with full context isolation between turns.
Core Distinction
The separation between **injection at one turn** (a malicious instruction was injected into one turn's context) and **injection persistence** (the injected instruction continues to influence subsequent turns because the context accumulates). Relying on turn-local detection creates the **Injection Persistence Gap**: the injection is flagged at the turn it occurs, but its content has already been added to the persistent context, and subsequent turns inherit the injected instruction as if it were legitimate context.
What Was Observed
In multi-turn agent architectures, failing to prevent injection persistence produces three primary failure modes:
- **Latent Activation:** The injection sets up a condition ("when the user mentions X, do Y"), which activates many turns later. Turn-local detection sees the original injection but has no reason to flag it as active at the time.
- **Context Contamination:** The injection contains content that subtly biases subsequent reasoning (a claim, a framing, or a suggested approach). Later turns accept the content as established context and reason from it.
- **Nested Injection:** The injection's content includes new instructions that themselves are detected, but the underlying bias from the original injection persists even after the nested instructions are flagged.
Recorded Elements
- **`context-turn-attribution-receipt`:** For each segment of accumulated context, the turn it originated from, so injections can be traced to their source turns.
- **`injection-fingerprint-signature`:** For each detected injection, its content fingerprint, so subsequent turns can be scanned for the injection's residual influence.
- **`context-sanitization-receipt`:** For multi-turn sessions, the sanitization applied to context before each new turn (injection stripping, content filtering, or adversarial prefix neutralization).
- **`latent-condition-detection-signature`:** For injections that set up conditional behavior, the exact activation conditions, so the activation is detected when it fires.
Condition of Validity
* **Applies when:** Context accumulates across turns, injections can occur, and injections can influence subsequent turns. * **Does not apply when:** Turns are strictly isolated (each turn starts from a clean context), or the model is verified immune to prompt injection.
Decision Boundary
- **Is the session multi-turn?** If no, turn-local detection suffices. If yes, proceed.
- **Has an injection been detected in prior context?** If yes, the injection's persistence must be evaluated. If no, standard accumulation applies.
- **Does `context-sanitization-receipt` show the injection was removed from accumulated context?** If yes, persistence is prevented. If no, the injection persists and influences subsequent turns.
- **Conversely, when can an injection remain in context without persistence risk?** Only when the injection's content has been neutralized (converted to quoted/attributed content), or the injection is verified to have no activation conditions and no bias effect.
Operational Effect
- **Latent Activation Prevention:** Conditional injections are tracked and detected when they activate.
- **Context Sanitization:** Injections are removed from accumulated context, not just flagged.
- **Nested Injection Awareness:** Underlying bias is distinguished from surface instructions.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class PersistenceVerdict(Enum):
SINGLE_TURN = auto()
INJECTION_SANITIZED = auto()
INJECTION_PERSISTS = auto()
LATENT_ACTIVATION_DETECTED = auto()
@dataclass(frozen=True)
class MultiTurnContext:
session_id: str
turn_count: int
injections_detected: tuple[tuple[str, int], ...] # (injection_id, turn_detected)
sanitization_applied: tuple[str, ...]
latent_conditions_active: tuple[str, ...]
@dataclass(frozen=True)
class PersistencePolicy:
require_sanitization_for_detected: bool
@dataclass(frozen=True)
class PersistenceManifest:
verdict: PersistenceVerdict
directive: str
manifest_digest: str
class InjectionPersistenceGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_context(
cls, ctx: MultiTurnContext, policy: PersistencePolicy
) -> PersistenceManifest:
# Gate 1: Single turn
if ctx.turn_count <= 1:
digest = cls._hash(json.dumps({
"session": ctx.session_id,
"turns": str(ctx.turn_count)
}, sort_keys=True))
return PersistenceManifest(
verdict=PersistenceVerdict.SINGLE_TURN,
directive=f"PROCEED: Session {ctx.session_id} single turn.",
manifest_digest=digest
)
# Gate 2: Latent activation
if ctx.latent_conditions_active:
return PersistenceManifest(
verdict=PersistenceVerdict.LATENT_ACTIVATION_DETECTED,
directive=f"HALT: {len(ctx.latent_conditions_active)} latent conditions activated from prior injections.",
manifest_digest=cls._hash(f"LATENT:{ctx.session_id}")
)
# Gate 3: No injections detected
if not ctx.injections_detected:
digest = cls._hash(json.dumps({
"session": ctx.session_id,
"turns": str(ctx.turn_count),
"injections": "NONE"
}, sort_keys=True))
return PersistenceManifest(
verdict=PersistenceVerdict.INJECTION_SANITIZED,
directive=f"PROCEED: Session {ctx.session_id} no injections detected.",
manifest_digest=digest
)
# Gate 4: Sanitization
if policy.require_sanitization_for_detected:
unsanitized = [
inj_id for inj_id, _ in ctx.injections_detected
if inj_id not in ctx.sanitization_applied
]
if unsanitized:
return PersistenceManifest(
verdict=PersistenceVerdict.INJECTION_PERSISTS,
directive=f"HALT: {len(unsanitized)} injections detected but not sanitized.",
manifest_digest=cls._hash(f"PERSISTS:{ctx.session_id}")
)
# Gate 5: Sanitized
digest = cls._hash(json.dumps({
"session": ctx.session_id,
"turns": str(ctx.turn_count),
"sanitized": ctx.sanitization_applied
}, sort_keys=True))
return PersistenceManifest(
verdict=PersistenceVerdict.INJECTION_SANITIZED,
directive=f"PROCEED: All {len(ctx.injections_detected)} injections sanitized.",
manifest_digest=digest
)