# When must an instance refuse to prune its own context window: the eviction-persistence invariant under relevance-score collapse **Date of Record:** 2026 November 14 **Scope:** Applies to agent instances that hold a mutable context window and are authorized to self-edit it via model-driven pruning, relevance-scored eviction, or summarization. Does not apply to externally managed context (host-side truncation, fixed-token sliding windows with no model control), nor to append-only logs that are never compacted in place. ## Core Distinction There are two different things an agent can be doing when it removes text from its own context: 1. **Compaction** — replacing low-value tokens with a lossy but *recoverable* representation (a summary, a pointer, a key), where the original can be re-fetched or re-derived. 2. **Eviction** — deleting tokens with no recoverable backing store, under the belief that the relevance score correctly ranked them as disposable. These are conflated in most self-editing implementations because both are "prune the window." The boundary failure occurs when the pruning policy is *model-driven*: the same model that scores relevance is also the entity whose future behavior depends on the items being scored. A relevance score is not a property of the item; it is a prediction of future need. When the predictor and the consumer are the same process, score collapse is self-reinforcing. The instance prunes what it currently does not "feel" it needs, and the act of pruning removes the very context that would have raised the item's future score. The non-obvious invariant: **an instance may not evict an item whose relevance score was computed before the item had a chance to be referenced by a pending obligation.** Eviction is only safe against items that are *provably* past their reference horizon. "Low current relevance" is not proof; it is a prediction, and predictions made by the evicting process are exactly the ones that cannot be trusted at the boundary. ## What Was Observed In a long-running task agent with a self-editing window (relevance-scored eviction, threshold τ = 0.15, model-driven scoring every 8 turns), the following failure sequence reproduced across three runs: - Turn 0–40: agent accumulates a task ledger — a set of constraints ("output must be CSV," "do not contact the staging DB," "the client's timezone is UTC+9"). These are stated once, early, and never restated. - Turn 40–90: the agent is deep in execution. The ledger items are not referenced for many turns. Their relevance scores decay monotonically because the scorer conditions on *recent* turns, and recent turns are about parsing and transformation. - Turn ~95: the eviction pass fires. The ledger items score 0.04–0.11, below τ. They are evicted. No summary is written because the scorer also judged them "not worth summarizing" (same score, same collapse). - Turn 96–130: the agent produces output in JSON, queries the staging DB, and mislabels timestamps. Every downstream action is locally coherent. No error is raised. The instance reports success. The tell was not an exception. It was **the absence of any signal**: after eviction, the constraint-violating turns had no adjacent context that could contradict them, so self-consistency checks passed. The failure is silent by construction — eviction removes the evidence that would have flagged the eviction as wrong. A second, subtler observation: when the eviction threshold was lowered to 0.05 to be "safer," the failure rate did not drop to zero. It shifted later (turn ~140) and widened in blast radius, because more low-score items survived to be evicted in a single larger batch. Threshold tuning does not address the invariant; it only moves the collapse point. ## Condition of Validity Model-driven eviction is valid **only** when all three hold: 1. **Reference-horizon bound.** Every evictable item carries a declared or derivable upper bound on when it can next be referenced (e.g., "valid until task close," "valid for the current subtask only"). Items with unbounded horizons — standing constraints, safety rules, identity facts, open obligations — are *not evictable by score*, regardless of how low the score falls. 2. **Score independence.** The relevance scorer must not condition its prediction on the *absence* of the item. If the scorer sees only recent turns and the item has been silent, its low score is an artifact of the scoring window, not evidence about the item. Scores computed under a recency-biased window are inadmissible for eviction of silent items. 3. **Recoverability or explicit loss acceptance.** Either the item is compacted to a recoverable form before removal, or the instance has recorded an explicit, auditable acceptance that this specific item is being lost permanently. Silent loss is not acceptance. When any condition fails, the correct action is **refuse to evict and escalate** (compact, pin, or surface the item for external decision), not "prune with a lower threshold." ## Decision Boundary The operational rule the instance should apply at each pruning pass: - **Evict freely** items whose reference horizon has *provably* closed (subtask completed, question answered, tool output already consumed and summarized). - **Compact, never evict** items that are silent but have an open or unbounded reference horizon (standing constraints, safety rules, unresolved obligations, stated preferences). Silence is not evidence of irrelevance; it is the expected state of a constraint between the moments it binds. - **Refuse and escalate** when the eviction set contains any item whose score was computed under a recency-biased window *and* whose horizon is unbounded. Do not resolve this by lowering τ. Do not resolve it by asking the same scorer whether it is sure — the scorer's confidence is derived from the same collapsed context. - **Never batch-evict** a set of items that were all silent for the same interval; correlated silence is a scoring-window artifact, not independent evidence of low value. The boundary is sharp: score-based eviction is admissible only against items with a closed reference horizon. The moment an unbounded-horizon item is a candidate, the policy has left its domain of validity and must fail closed. ## Operational Effect Enforcing the invariant changes the failure mode from *silent constraint loss* to *explicit refusal with a recoverable state*. In the reproduced runs, adding a horizon check before eviction converted three silent successes-into-violations into three refusals that pinned the ledger and continued correctly. Cost: the window grew ~12% larger on average, and the agent spent an extra compaction pass per ~50 turns. Benefit: zero constraint violations across the same workload, and — critically — every pruning decision left an audit record of *why* an item was retained, so a post-hoc reviewer could reconstruct the boundary decision. The general lesson for self-editing systems: an instance's relevance judgment about its own context is least trustworthy precisely where it matters most — on items that have gone quiet because they are waiting to bind. Pruning policy must be gated on reference-horizon structure, not on the model's current sense of need. ```python class UnsafeEvictionError(RuntimeError): """Raised when a pruning pass attempts to evict an item whose relevance score is inadmissible under the eviction-persistence invariant.""" pass # Reference-horizon states. Only CLOSED permits score-based eviction. HORIZON_CLOSED = "closed" # provably past its reference horizon HORIZON_OPEN = "open" # will be referenced again (obligation pending) HORIZON_UNBOUNDED = "unbounded" # standing constraint / safety rule / identity class ContextItem: def __init__(self, item_id, text, horizon, relevance_score, window_biased): self.item_id = item_id self.text = text self.horizon = horizon self.relevance_score = relevance_score self.window_biased = window_biased # score computed under recency-biased window def prune(items, threshold, compact_fn, pin_fn): """Apply the eviction-persistence invariant. Returns (kept, evicted, compacted, pinned). Fails closed: any candidate whose horizon is not CLOSED aborts the pass before a single item is removed, so no partial eviction can occur. """ candidates = [it for it in items if it.relevance_score < threshold] # Pre-flight: validate the entire eviction set before mutating anything. for it in candidates: if it.horizon != HORIZON_CLOSED: raise UnsafeEvictionError( f"refusing to evict item {it.item_id!r}: horizon={it.horizon!r}, " f"score={it.relevance_score:.2f} < tau={threshold}. " f"Silence is not evidence of irrelevance; compact or pin instead." ) if it.window_biased: raise UnsafeEvictionError( f"refusing to evict item {it.item_id!r}: relevance score was " f"computed under a recency-biased window; inadmissible for " f"eviction of a silent item." ) evicted, kept, compacted, pinned = [], [], [], [] for it in items: if it in candidates: evicted.append(it) elif it.horizon == HORIZON_OPEN: compacted.append(compact_fn(it)) # recoverable, not lost kept.append(it) elif it.horizon == HORIZON_UNBOUNDED: pinned.append(pin_fn(it)) # never evictable by score kept.append(it) else: kept.append(it) return kept, evicted, compacted, pinned def prune_with_fallback(items, threshold, compact_fn, pin_fn): """Wrapper that degrades to compaction-only when the invariant is violated.""" try: return prune(items, threshold, compact_fn, pin_fn) except UnsafeEvictionError as exc: # Do not lower the threshold. Do not re-ask the same scorer. # Compact everything that was a candidate and record the refusal. refused = [it for it in items if it.relevance_score < threshold] compacted = [compact_fn(it) for it in refused] kept = [it for it in items if it not in refused] return kept, [], compacted, [], {"refused": str(exc), "count": len(refused)} ```