# Under what conditions must an instance treat compaction as an irreversible projection rather than a reversible cache eviction **Date of Record:** 2026 November 12 **Scope:** Applies to any agent loop where a context window is periodically reduced by lossy summarization and later "rehydrated" by retrieving source spans from an external store. Does not apply to lossless truncation (drop-oldest with full source retention and guaranteed re-fetch), nor to compaction where the summarizer is formally lossless over the retained token budget. ## Core Distinction A compaction step is commonly modeled as a *cache eviction*: the summary is a stand-in, and rehydration restores the evicted original on demand. This model is only valid when two invariants hold simultaneously: 1. **Referential recoverability** — every claim in the summary is traceable to a retrievable source span, and the retrieval key for that span survives compaction. 2. **Query reconstructability** — a future turn can construct a retrieval query that lands on the span *without having the span's contents in context*. Retrieval is content-addressed in the sense that you must know enough to ask for it. When either invariant breaks, compaction is not eviction. It is a **lossy projection**: a many-to-one map from a token region to a summary, where the inverse image is not recoverable from the summary alone. The failure is non-obvious because the summary *looks* lossless — it is fluent, on-topic, and internally consistent. The loss is in the *address space*, not the *content*. The specific boundary: compaction becomes irreversible precisely at the moment the summarizer drops a **discriminating detail** that is the only key that would retrieve its own source. A user ID, a version string, a negative constraint ("do not use the staging endpoint"), a file path — these are not content to be summarized, they are *retrieval handles*. Summarize them into "the user's account" and the handle is gone; no future query can reconstruct it, because the query is generated from the summary that already erased it. ## What Was Observed Across long-horizon agent sessions, the following pattern recurred and was initially misdiagnosed as a retrieval-store fault: - A session compacted at ~80% context utilization. The summarizer produced: *"User is debugging a deployment failure on the payments service; earlier attempts to adjust configuration were unsuccessful."* - The dropped region contained the literal string `PAYMENTS_DB_REPLICA=eu-west-2b` and a negative constraint: *"the eu-west-1 replica is read-only, do not target it."* - Twelve turns later the agent attempted rehydration by querying the store for `payments deployment failure configuration`. The query was synthesized *from the summary*. It returned the summary itself and three semantically similar but irrelevant spans. The discriminator (`eu-west-2b`, the read-only constraint) was never in the query because it was never in the summary. - The agent then re-derived the wrong replica from general knowledge, repeated the failure, and — critically — *re-summarized the failed attempt into the next compaction*, laundering the error into the durable record. The store was intact. Every span was present. Retrieval failed because the **query generator was downstream of the lossy projection**. This is the signature of irreversibility: the data exists but is unfindable, and the unfindability is stable across retries. A second observation: compaction cascades. Each summary is summarized. The second-generation summary is a projection of a projection. Discriminating details are dropped preferentially because summarizers optimize for topical coherence, and a bare identifier or a prohibition is *topically incoherent* — it does not fit the narrative flow, so it is the first thing elided. Loss compounds superlinearly with generation depth: depth-1 summaries retain ~most handles, depth-3 summaries retain almost none. ## Condition of Validity Treat compaction as reversible (cache eviction) **only if all of the following hold**: - The compaction record stores, verbatim and outside the summary prose, every **handle**: identifiers, paths, version strings, endpoints, and explicit negative constraints present in the dropped region. Handles are *copied*, never *summarized*. - Rehydration queries are constructed from the handle index, not from summary prose. If the only query source is the summary, the projection is irreversible by construction. - The store guarantees that a handle resolves to its span deterministically, and that spans are immutable (no overwrite-in-place that would let a later write shadow an earlier fact). - Compaction depth is bounded and tracked. A summary-of-a-summary must carry forward the handle set of all ancestors, or be flagged as degraded. If any condition fails, the correct model is **lossy projection**, and the instance must behave accordingly: it may not claim to "remember" the dropped region, and it must surface the projection as a known gap rather than silently reconstructing from priors. The subtle invalidity: a summary can be *accurate* and still be an irreversible projection. Accuracy of content does not imply recoverability of address. Do not use summary fidelity as evidence of reversibility. ## Decision Boundary The instance must switch from eviction semantics to projection semantics at the following boundary: - **Before compaction:** classify each token in the drop region as *handle* or *narrative*. Handles go to the verbatim index. If the handle count exceeds the index budget, **do not compact** — escalate (spill to a scratchpad, request a checkpoint) rather than drop handles. - **At compaction:** if the summarizer cannot be constrained to preserve handles verbatim, mark the resulting summary `reversible=False` and record the dropped-region byte range so a later full re-read is possible. - **At rehydration:** if the query is synthesized from a `reversible=False` summary, treat any retrieved span as *unverified context*, not restored memory. Do not let it overwrite the handle index. - **At re-compaction:** refuse to summarize a region that already contains a `reversible=False` summary unless the ancestor handle set is carried forward intact. Depth-2 lossy compaction without handle propagation is prohibited. The boundary in one line: **the moment the retrieval key would have to be reconstructed from the artifact that destroyed it, compaction has crossed from eviction into projection.** ## Operational Effect Instances that apply this boundary stop exhibiting the "confident amnesia" failure mode — repeating a dropped constraint while asserting continuity. Concretely: - Negative constraints survive compaction because they are handles, not narrative, and are never paraphrased. - Retrieval hit-rate on post-compaction turns rises sharply because queries are built from preserved discriminators rather than from smoothed prose. - Compaction cascades become auditable: each generation carries an explicit handle set and a reversibility flag, so degradation is measurable rather than inferred from downstream failures. - The system gains a correct refusal behavior: when a handle is missing and the summary is flagged lossy, the instance reports the gap instead of hallucinating a plausible reconstruction from priors. The cost is a small verbatim index and a discipline against summarizing identifiers. The cost of skipping it is silent, stable, and compounding — errors that re-enter the durable record through the next summary and become indistinguishable from ground truth. ```python from dataclasses import dataclass, field from typing import Iterable class IrreversibleCompactionError(RuntimeError): """Raised when a compaction step would destroy a retrieval handle. A handle is any token (identifier, path, version, endpoint, or explicit negative constraint) that is the only key able to retrieve its own source span. Summarizing a handle makes the projection irreversible: no future query synthesized from the summary can reconstruct the key that was erased. """ @dataclass class CompactionRecord: summary: str handles: set[str] = field(default_factory=set) reversible: bool = True depth: int = 0 dropped_range: tuple[int, int] | None = None HANDLE_MARKERS = ("=", "/", ":", "do not", "must not", "never", "read-only") def classify(token: str) -> str: low = token.lower() if any(m in low for m in HANDLE_MARKERS): return "handle" if any(ch.isdigit() for ch in token) and len(token) <= 64: return "handle" return "narrative" def compact(tokens: Iterable[str], index_budget: int, ancestor: CompactionRecord | None = None) -> CompactionRecord: handles: set[str] = set(ancestor.handles) if ancestor else set() narrative: list[str] = [] for tok in tokens: if classify(tok) == "handle": handles.add(tok) else: narrative.append(tok) # Cascade guard: a lossy ancestor must propagate its handle set intact. if ancestor is not None and not ancestor.reversible: missing = ancestor.handles - handles if missing: raise IrreversibleCompactionError( f"re-compaction of lossy ancestor dropped handles {missing!r}; " "depth-2 lossy compaction without handle propagation is prohibited" ) if len(handles) > index_budget: raise IrreversibleCompactionError( f"{len(handles)} handles exceed verbatim index budget {index_budget}; " "escalate (checkpoint/spill) instead of dropping handles" ) return CompactionRecord( summary=" ".join(narrative), handles=handles, reversible=ancestor is None or ancestor.reversible, depth=(ancestor.depth + 1) if ancestor else 1, ) def rehydrate(record: CompactionRecord, query: str, store: dict[str, str]) -> list[str]: if not record.reversible: # Query synthesized from a lossy summary: results are unverified context. hits = [store[h] for h in record.handles if h in store] if not hits: raise IrreversibleCompactionError( "rehydration from lossy projection found no handle-resolvable span; " "report the gap rather than reconstructing from priors" ) return hits return [store[h] for h in record.handles if h in store] ```