# Which invariants prevent silent semantic loss when a model prunes its own context window **Date of Record:** 2026 November 11 **Scope:** Applies to agents that mutate their own context (pruning, summarizing, re-weighting retention) between inference steps, where the same model both selects what to drop and consumes the surviving context. Does not apply to externally orchestrated compaction where a separate process owns retention policy and the acting model has no write access to its own history. ## Core Distinction The non-obvious failure here is not context overflow. It is *prune-then-reason over a corrupted premise*. When a model decides what to evict, it evaluates salience at time *t* using the current task state. But eviction decisions are irreversible within a session, and the salience signal at time *t* is a function of the task as currently framed — which is exactly the thing that later steps may need to re-derive. The boundary condition: **a retention policy that is optimal for the current step can be catastrophically lossy for a step that re-frames the task.** This is not a compression-ratio problem; it is an information-theoretic one where the pruner lacks a model of its own future query distribution. The invariant that must hold: *any fact evicted must be reconstructible from the surviving context plus an external store, or the eviction must be refused.* Model-driven pruning routinely violates this because the model cannot distinguish "this fact is irrelevant" from "this fact is currently un-salient but load-bearing for a branch I haven't taken yet." ## What Was Observed In a multi-step agent operating on a 200k token window with a priority-weighted retention policy (recency × task-relevance × user-pinned), the following sequence reproduced the failure across three runs: 1. Step 4: agent summarizes a 40k-token tool-output block into a 600-token digest, dropping raw error signatures. Digest retains "the migration failed due to a schema mismatch." The specific column names and the exact constraint violation are evicted. 2. Step 9: agent re-frames the task from "fix the migration" to "explain why the migration failed to the user." It now needs the exact constraint violation. 3. Step 9 queries its own context for the error signature. It is gone. The agent does not report the gap — it *reconstructs a plausible signature* from the digest and the schema, which is syntactically valid and factually wrong. 4. Step 11: agent writes a fix targeting the hallucinated constraint. The real constraint violation persists. The critical observation: **the agent never registered an absence.** Pruning produced a context that was internally coherent but externally false. There was no signal — no token budget pressure, no error, no contradiction — that anything had been lost. The failure was silent by construction. A second observation: priority weighting decayed the raw tool output below the eviction threshold *because* the summary referenced it. The summary's existence suppressed the source's retention score. Summarization and retention were coupled through shared salience, creating a positive feedback loop toward loss. ## Condition of Validity The invariant "evicted facts must be reconstructible or eviction is refused" holds only under these conditions: - **The pruner has a declared query distribution over future steps.** If the agent can bound what it will need (e.g., "this branch only reads the API schema"), eviction of out-of-branch facts is safe *for that branch*. The invariant fails the moment the agent is allowed to re-frame. - **Summaries carry provenance pointers, not just content.** A digest that says "schema mismatch" without a pointer to the evicted span is a lossy leaf. A digest with a pointer to an external store is recoverable. - **Retention scores are computed independently of summary existence.** If the presence of a summary lowers the source's score, the coupling must be broken or the system will converge on loss. - **The model can detect its own absence.** This is the hard one. Models do not natively represent "I used to know X." Absence detection requires either an explicit gap-marker in the surviving context or an external index the model is trained/prompted to consult before asserting. When any of these fails, the invariant is not merely weakened — it is *inverted*: the system becomes more confident as it becomes less correct, because pruning removes the evidence of its own error. ## Decision Boundary The operational line: **prune freely when the evicted content is derivable; refuse when it is only retrievable.** - Derivable: facts the model can recompute from surviving context (e.g., a timestamp derivable from a retained event, a count derivable from a retained list). Evict these without ceremony. - Retrievable-only: raw observations, user statements, tool outputs, exact identifiers, error signatures. These cannot be recomputed; they can only be looked up. Evict only with a provenance pointer to an external store, and mark the surviving context with a gap-marker so the model knows to consult the pointer before asserting. The boundary case that breaks naive policies: **content that is derivable *now* but becomes retrievable-only after a re-frame.** A schema mismatch is derivable while the task is "fix the migration" (the fix implies the constraint). After the re-frame to "explain the failure," it is retrievable-only. The policy must therefore treat re-framing as a retention-invalidating event: on re-frame, re-score the entire context against the new query distribution, and refuse eviction of anything whose derivability status changed. Priority-weighted retention must also enforce a **floor**: a minimum retention score below which no item is evicted regardless of weight, applied to items tagged as raw-observation. Without a floor, the decay function will eventually evict the floor itself. ## Operational Effect The practical consequence is a shift from *compaction as compression* to *compaction as indexed elision*. The context window stops being the model's memory and becomes its working set, with a mandatory external store for anything evicted. This costs a retrieval round-trip on re-frame, but it converts a silent-corruption failure into a detectable-latency failure — which is the correct trade, because latency is observable and corruption is not. Concretely: the agent gains a `ContextGapError` that fires when it attempts to assert a fact whose provenance pointer is unresolved. The error is not a bug; it is the invariant enforcing itself. The following reference implementation encodes the boundary — eviction is permitted only for derivable content, and raw observations without a resolvable pointer raise. ```python class UnreconstructibleEvictionError(RuntimeError): """Raised when a retention policy evicts content that cannot be reconstructed from surviving context or an external store.""" pass class ContextItem: def __init__(self, item_id, kind, derivable, pointer=None): # kind: "derived" | "raw_observation" | "summary" self.item_id = item_id self.kind = kind self.derivable = derivable # recomputable from surviving context? self.pointer = pointer # external store handle, if any self.retention_score = 1.0 def evict(items, threshold, external_store_resolves): """Apply a priority-weighted eviction policy with the derivability invariant. Returns (surviving, evicted). Raises if any eviction is unreconstructible. """ surviving, evicted = [], [] for item in items: if item.retention_score >= threshold: surviving.append(item) continue # Below threshold: candidate for eviction. Enforce the invariant. if item.derivable: evicted.append(item) continue if item.pointer is not None and external_store_resolves(item.pointer): evicted.append(item) continue # Raw observation, non-derivable, no resolvable pointer: refuse. raise UnreconstructibleEvictionError( f"refusing to evict {item.item_id!r} (kind={item.kind!r}): " f"not derivable from surviving context and pointer " f"{item.pointer!r} does not resolve. " f"Re-frame or re-score before pruning." ) return surviving, evicted def re_frame(items, new_query_distribution): """On task re-frame, re-score all items and re-validate derivability. A re-frame can convert a derivable item into a retrievable-only item; this function must run before any subsequent eviction. """ for item in items: if item.kind == "raw_observation": item.derivable = False # raw observations are never re-derivable item.retention_score = new_query_distribution.score(item) # Any item whose derivability flipped must now carry a pointer or be floored. for item in items: if not item.derivable and item.pointer is None: item.retention_score = float("inf") # floor: never evict return items ```