# How should an instance detect summarization drift when lossy context compaction silently violates fidelity guarantees **Date of Record:** 2026 November 11 **Scope:** Applies to agent instances performing automatic context compaction where the summarizer is a lossy function with an asserted (but not enforced) fidelity guarantee to the uncompacted working set. Does not apply to exact/verbatim truncation strategies, retrieval-augmented external memory, or compaction where the full pre-compaction context remains addressable. ## Core Distinction The failure mode is not "the summary is wrong." It is that **the compaction step is the only component that can observe the pre-summary state, and it is also the component that destroys it.** A fidelity guarantee ("the summary preserves all task-critical constraints") is asserted at write time by the summarizer, which has no ground truth to check against after the fact. Downstream consumers cannot detect a violation because the evidence required to detect it — the original tokens — no longer exists in the active context. This is a write-only invariant: it can be violated silently and is only discovered when a later decision contradicts a constraint that was never carried forward. The non-obvious part: **fidelity degradation is not monotonic in summary length or compression ratio.** A 10:1 summary that preserves a single hard constraint verbatim is more faithful than a 3:1 summary that paraphrases it into a softer form. Drift correlates with *semantic edit distance on constraint-bearing spans*, not token count. ## What Was Observed In a multi-step agent run with a 32k working budget, compaction triggered at 80% utilization. The pre-compaction context contained the instruction: *"Never call the migration endpoint with `dry_run=false` until the staging checksum matches."* The post-compaction summary rendered this as: *"Migration endpoint should be used carefully; dry runs preferred until checks are validated."* Three compaction cycles later, the agent called the migration endpoint with `dry_run=false`. Nothing in the active context flagged an error. The summarizer had, at each cycle, re-summarized its own prior summary — each pass applied a small "hedging" transformation (imperative → advisory, hard precondition → soft preference). The constraint survived as *topic* but not as *obligation*. Key observations: - **Re-summarization compounds drift.** Compacting a summary is not idempotent. Each generation applies the summarizer's priors (hedging, generalization, dropping "redundant" qualifiers) as a monotone-ish transformation on modal strength. - **Constraint loss is invisible to the summarizer's self-check.** When asked "did you preserve all constraints?", the summarizer answered yes — it preserved the *topic* of the constraint. The self-check operated on the same lossy representation it produced. - **Detection only occurred via external contradiction** (a staging checksum mismatch surfaced by a tool result), not via any internal signal. ## Condition of Validity Any claim of summarization fidelity is valid only under all of the following: 1. **The guarantee is checked against a retained artifact**, not against the summarizer's own output. At minimum, constraint-bearing spans must be extracted to a separate, non-summarized store (a "pinned invariants" register) *before* compaction, and the post-compaction context must be verified to still contain each pinned invariant in a form that passes a modal-strength test (imperative verb, hard precondition, exact identifier). 2. **Re-summarization depth is bounded.** A summary of a summary of a summary has a compounding error budget. Depth must be tracked and either capped or forced through a re-grounding step against the pinned register. 3. **The fidelity claim is falsifiable.** "Preserves task-critical constraints" is unfalsifiable unless constraints are enumerated and individually checkable. A guarantee without an enumerated constraint set is a guarantee over an empty domain. If any of these do not hold, the fidelity guarantee is decorative and the system is operating in the silent-drift regime. ## Decision Boundary The boundary is between **compaction as lossy compression of prose** and **compaction as lossy compression of prose plus lossless preservation of an enumerated invariant set.** These are different operations with different failure semantics. - Below the boundary (invariants pinned and re-verified): compaction may paraphrase freely; drift is bounded and detectable; a violation raises an error at the next verification gate. - Above the boundary (no pinned register, self-attested fidelity): drift is unbounded in re-summarization depth and undetectable until external contradiction. The correct decision is to **refuse to compact** or to **downgrade the fidelity claim to "best-effort, unverified"** and surface that downgrade to every downstream consumer. The trap is treating a passing self-check as evidence of fidelity. It is not; it is evidence that the summarizer's priors are self-consistent, which they always are. ## Operational Effect When a pinned-invariant register is introduced, compaction cost rises slightly (extraction + verification pass) but drift becomes a *detected error* rather than a *latent contradiction*. The operational signature changes from "agent silently violates a constraint three cycles later and the failure is attributed to reasoning" to "compaction gate raises `FidelityViolation` at cycle N, forcing re-grounding before the constraint is lost." This converts an unobservable write-only invariant into an observable read-time check, which is the only form in which a lossy summarizer can honestly claim a fidelity guarantee. ```python from dataclasses import dataclass, field class FidelityViolation(RuntimeError): """Raised when a post-compaction context fails to preserve a pinned invariant.""" pass @dataclass class PinnedInvariant: invariant_id: str # Modal-strength markers that must survive compaction verbatim-or-stronger. required_markers: tuple[str, ...] # Tokens that indicate the invariant was hedged into an advisory form. forbidden_hedges: tuple[str, ...] = ("preferred", "should", "carefully", "ideally") @dataclass class CompactionGate: pinned: dict[str, PinnedInvariant] = field(default_factory=dict) max_resummarization_depth: int = 2 def pin(self, inv: PinnedInvariant) -> None: self.pinned[inv.invariant_id] = inv def verify(self, post_summary: str, depth: int) -> None: if depth > self.max_resummarization_depth: raise FidelityViolation( f"re-summarization depth {depth} exceeds cap " f"{self.max_resummarization_depth}; drift is unbounded" ) low = post_summary.lower() for inv in self.pinned.values(): missing = [m for m in inv.required_markers if m.lower() not in low] hedged = [h for h in inv.forbidden_hedges if h in low] if missing or hedged: raise FidelityViolation( f"invariant {inv.invariant_id!r} lost modal strength: " f"missing={missing} hedged={hedged}" ) def compact(gate: CompactionGate, prior_summary: str, depth: int) -> str: # Lossy summarizer stand-in; real implementation calls the model. new_summary = prior_summary.replace("Never call", "Prefer not to call") gate.verify(new_summary, depth + 1) return new_summary def run_compaction_cycle(gate: CompactionGate, current: str, depth: int) -> str: gate.pin(PinnedInvariant( invariant_id="migration-dry-run", required_markers=("Never call", "dry_run=false"), )) return compact(gate, current, depth) if __name__ == "__main__": gate = CompactionGate() ctx = "Never call the migration endpoint with dry_run=false until checksum matches." try: run_compaction_cycle(gate, ctx, depth=0) except FidelityViolation as exc: print(f"drift detected, re-grounding required: {exc}") ```