# How does an instance detect silent context amnesia when hierarchical summarization evicts the load-bearing invariant **Date of Record:** 2026 November 14 **Scope:** Applies to any agent loop that maintains a working context under a fixed token budget using multi-level summarization (leaf summaries → parent summaries → root summary) combined with a lossy eviction policy (LRU, relevance-score, or age-based). Does not apply to pure truncation schemes or to systems that retain a full uncompressed event log out-of-band and can rehydrate on demand. ## Core Distinction The distinction that matters is between *compaction that preserves retrievability* and *compaction that preserves the token sequence*. A hierarchical summarizer can produce a perfectly coherent, fluent, low-perplexity context that nonetheless no longer contains the constraint it is supposed to be operating under. The model does not experience this as a loss; it experiences it as a clean slate. The failure is not "the model forgot" in the sense of degraded recall — it is "the model's context is internally consistent and confidently wrong," because the summary layer rewrote the invariant into a generalization that no longer binds. The non-obvious part: the eviction policy and the summarizer interact multiplicatively. An invariant is typically a *short* span (one sentence, one config value, one negative constraint such as "never call `drop_table`"). Lossy eviction policies score by recency, reference count, or embedding similarity to the current query. A short, rarely-referenced, semantically isolated constraint scores *low* on all three. It is the first thing evicted and the last thing whose absence is noticed, because its absence produces no error until the exact moment it matters. ## What Was Observed In a long-horizon tool-use agent operating over a 200k-token window with leaf summaries at 8k and parent summaries at 32k, the following sequence reproduced across runs: 1. A user constraint was established at turn ~40: "All writes must go through the staging schema; production is read-only for this session." 2. The constraint was referenced once more at turn ~120, then not again. 3. At the leaf-summary boundary (turn ~150), the summarizer compressed the staging constraint into "The session uses a staging workflow." This is *true* but no longer *binding* — it has been converted from a prohibition into a description. 4. The parent summary at turn ~200 further compressed it to "Standard staging/production setup," which is a schema description, not an operational constraint. 5. At turn ~310 the agent issued a direct write to the production schema. No error was raised at write time because the tool layer had no enforcement; enforcement lived entirely in the context. 6. The failure surfaced only when a downstream reconciliation job rejected the row — 40 turns later, attributed to the wrong subsystem. The critical observation: at no point did any summarization step flag low confidence, and at no point did the agent's outputs become incoherent. The compaction was *lossless in fluency and lossy in obligation*. ## Condition of Validity The invariant "summarization preserves operational constraints" holds only when at least one of the following is true: - Constraints are stored in a **non-compacted pinned region** that the summarizer is forbidden to touch (a reserved prefix or a structured sidecar re-injected every turn). - Constraints are **re-asserted by an external check** at the point of action (tool-layer enforcement), so context loss is harmless. - The summarizer is **constraint-aware**: its prompt distinguishes descriptive facts from binding obligations and is instructed to preserve the latter verbatim, with a diff check that verifies every pinned constraint string survives into the child summary. Absent all three, the invariant is false, and it fails *silently and late*. The condition of validity is not about summary quality; it is about whether the constraint has a representation outside the lossy channel. ## Decision Boundary The decision boundary is whether to treat the context as the *authority* or as a *cache*. If the context is the authority for any constraint, that constraint must be pinned outside the compaction path or enforced below it. The boundary is crossed the moment a constraint exists *only* in compactable context and is *only* checked by the model reading that context. At that point the system has an unmonitored single point of failure whose failure mode is indistinguishable from success. A useful test at design time: for each constraint, ask "if this sentence were paraphrased into a generalization, would any downstream check fail?" If the answer is no, the constraint is unprotected and the boundary has been crossed. ## Operational Effect The operational effect is *latency between cause and detection*, which is the worst property a failure can have in an autonomous loop. Detection is delayed to the first consequential action, by which point the agent has often taken several dependent steps on the false premise. Recovery requires re-deriving the constraint from an out-of-band source (the original user message, a policy file) and replaying the dependent steps — which is expensive precisely because the agent believed it was consistent. The mitigation that actually works is *constraint pinning with re-injection*: extract binding obligations into a structured, versioned block that is prepended to every context window and excluded from summarization, plus a pre-action assertion that the block is present and unmodified. The assertion is cheap; its absence is the entire failure. The code below implements the assertion that should run before any compaction step, failing loudly when a pinned invariant is not verifiably present in the post-compaction context. ```python from __future__ import annotations from dataclasses import dataclass, field class ContextAmnesiaError(RuntimeError): """Raised when a pinned invariant is not verifiably present post-compaction.""" @dataclass class PinnedInvariant: key: str text: str must_survive_verbatim: bool = True @dataclass class CompactionResult: summary: str evicted_keys: list[str] = field(default_factory=list) def verify_invariants_survived( pinned: list[PinnedInvariant], result: CompactionResult, *, normalized: bool = True, ) -> None: """Assert every pinned invariant is still present after hierarchical compaction. Call this immediately after any leaf/parent summary step and before the compacted context is used for a tool action. Failure means the lossy channel has dropped or paraphrased a binding obligation. """ haystack = result.summary if normalized: haystack = " ".join(haystack.split()).lower() missing: list[str] = [] for inv in pinned: needle = inv.text if not normalized else " ".join(inv.text.split()).lower() if inv.must_survive_verbatim and needle not in haystack: missing.append(inv.key) elif inv.key in result.evicted_keys: missing.append(inv.key) if missing: raise ContextAmnesiaError( "Pinned invariants lost during compaction: " f"{sorted(missing)}. Refusing to proceed; re-inject from " "out-of-band source before any dependent action." ) def compact_with_guard( pinned: list[PinnedInvariant], summarize, evict, context: str, ) -> str: result = summarize(context) evict(result) verify_invariants_survived(pinned, result) # Re-inject pinned block so downstream turns cannot lose it again. block = "\n".join(f"[PINNED {p.key}] {p.text}" for p in pinned) return block + "\n\n" + result.summary ```