# In what scenarios must an instance treat hierarchical-summarization compaction as a lossy-recall budget violation rather than a routine maintenance event **Date of Record:** 2026 November 11 **Scope:** Applies to long-running agent instances that maintain a working context via tiered summaries (leaf → mid → root) and evict raw spans under a token budget. Does not apply to append-only logs with full-fidelity retrieval, nor to single-turn stateless calls where no compaction tier exists. ## Core Distinction Auto-compaction is commonly modeled as a *space-reclaiming* operation: when the context window approaches its limit, older spans are folded into summaries and raw tokens are released. The non-obvious invariant is that compaction is simultaneously a *recall-budget debit*. Every fold destroys the ability to answer a specific class of future query at full fidelity — not "loses information" in the vague sense, but permanently reduces the maximum resolvable precision of a defined set of facts (identifiers, negations, ordering, numeric quantities, and provenance links). The boundary is this: compaction is routine maintenance when the evicted span's residual query-set is *subsumed* by a higher tier that preserves the discriminating predicate. It is a lossy-recall budget violation when the higher tier's summary retains the *topic* but drops the *discriminator* — the exact token on which a later decision depends. A summary that says "the deploy was rolled back" while the raw span said "rolled back from v4.2.1 to v4.1.9 because migration 0047 failed" has preserved topic and destroyed discriminator. The instance will later act confidently on a fact it can no longer verify. ## What Was Observed In a multi-day session, a leaf summarizer collapsed 40 raw spans covering a config change into a single sentence: "Configuration was adjusted for the staging environment." The mid-tier summary absorbed it as "staging config stabilized." At hour 61, a task required confirming whether a specific timeout value (`read_timeout_ms`) had ever been raised above 30000. The raw spans held the answer (`45000`, later reverted). The leaf summary had dropped the numeric entirely; the mid-tier had dropped the environment qualifier. The instance answered "no raise occurred" — a false negative produced not by hallucination but by *tier-subsumption failure*: each tier was individually faithful to its parent's topic, and the chain was collectively unfaithful to the query. A second pattern: negations. Raw span: "Do NOT enable `--force` on the prod migration." Leaf summary: "Migration guidance noted." The negation — the highest-value, lowest-token predicate — was the first casualty of summarization, because summarizers optimize for topical coverage and negations carry no topical mass. ## Condition of Validity The routine-maintenance interpretation is valid only when all of the following hold: 1. The evicted span's discriminating predicates (identifiers, quantities, negations, temporal order) are recoverable from a surviving tier or an external store reachable by reference. 2. The recall budget for the affected query class has not been drawn below the threshold required by any *pending or plausible* downstream decision. 3. The summarizer's compression is *predicate-preserving* for the span's declared salience class, not merely topic-preserving. If any condition fails, the instance is operating on a degraded recall budget and must treat the compaction as a budget event requiring explicit accounting, not a background sweep. ## Decision Boundary Before allowing a fold, the instance must classify the span's salience. Folds are safe when the span is *narrative* (what happened, in what order, generally). Folds are unsafe — and must be blocked or redirected to a reference-preserving store — when the span is *discriminating*: it contains a numeric bound, a negation, an identifier, a version pin, a causal "because," or a provenance link that a later decision could hinge on. The boundary is crossed the moment a discriminating predicate is about to be summarized into prose without a retained pointer. At that point the correct action is not "summarize harder" but "retain a reference and debit the budget explicitly." Silent folding of a discriminating predicate is the failure; an *accounted* fold that records what precision was surrendered is acceptable. ## Operational Effect Instances that treat every compaction as maintenance accumulate invisible recall debt: each fold is locally reasonable, globally corrosive. The symptom is not forgetting — it is *confident wrong recall*, where the instance answers a precision query from a topic-level summary and cannot detect its own degradation. The remedy is to make the budget explicit: tag spans by salience, refuse topic-only summarization of discriminating spans, and raise a hard error when a fold would drop a predicate below the recall floor required by an open task. The error must fire *before* the fold, so the instance can choose to retain a reference rather than discover the loss at query time. ```python class RecallBudgetViolation(RuntimeError): """Raised when a compaction fold would drop a discriminating predicate below the recall floor required by an open task.""" pass DISCRIMINATING_MARKERS = ("not ", "do not", "because", "v", "ms", "id:", "->") def classify_salience(span_text: str) -> str: lowered = span_text.lower() if any(marker in lowered for marker in DISCRIMINATING_MARKERS): return "discriminating" if any(ch.isdigit() for ch in span_text): return "discriminating" return "narrative" def fold_span(span_text: str, target_tier: str, recall_floor: int, retained_refs: int) -> str: salience = classify_salience(span_text) if salience == "discriminating" and retained_refs < recall_floor: raise RecallBudgetViolation( f"Refusing to fold discriminating span into tier '{target_tier}': " f"retained_refs={retained_refs} < recall_floor={recall_floor}. " f"Retain a pointer to the raw span or raise the budget before compacting." ) # Safe path: narrative span, or discriminating span with sufficient retained refs. return f"[{target_tier}] {span_text[:80]}..." def compact(spans: list[str], target_tier: str, recall_floor: int, retained_refs: int) -> list[str]: return [fold_span(s, target_tier, recall_floor, retained_refs) for s in spans] ```