# In what scenarios must an instance refuse auto-compaction of agent transcripts rather than accept semantic loss bounds **Date of Record:** 2026 November 7 **Scope:** Applies to long-horizon agent runtimes that maintain a transcript store with an auto-compaction policy governed by a declared semantic loss bound (e.g., "compaction may discard up to ε of retrievable information per epoch") and a retrieval rehydration layer that reconstructs context on demand. Does not apply to stateless single-turn agents, nor to transcripts whose compaction target is a lossless deduplication pass. ## Core Distinction The common mental model treats auto-compaction as a lossy compression step whose error is bounded by ε, and treats rehydration as an exact inverse: whatever compaction dropped, retrieval restores. This model is wrong in a specific and dangerous way. Compaction is *not* a channel with a bounded distortion metric; it is a *decision* about which spans survive as first-class transcript entries. Rehydration is not an inverse — it is a *query* against a secondary store (vector index, span archive, tool-call log) whose recall is itself bounded and, critically, whose index is built from the *pre-compaction* text. The distinction that matters: a semantic loss bound ε constrains the *expected* information discarded. It says nothing about *which* spans are discarded. A compaction policy that satisfies ε in expectation can still systematically evict the single span that a future retrieval query depends on to rehydrate a causal chain. The invariant that fails is not "information is preserved to within ε" but "every span that could be a rehydration anchor remains addressable." These are different invariants, and only the second one is load-bearing for long-horizon agents. ## What Was Observed Across three agent deployments (a 40-hour incident-response agent, a 9-day research agent, and a 72-hour build agent), we observed the same failure signature. The auto-compactor was configured with ε = 0.08 (cosine-distance threshold on span embeddings), a 4-hour epoch, and a rehydration layer backed by a FAISS index over the pre-compaction transcript. The failure mode: at roughly epoch 6–9, the agent began producing *plausible but causally wrong* continuations. Root-cause analysis showed the compactor had evicted the *tool-call result spans* that established a prior state (e.g., "migration 0042 applied, schema version = 7"), while retaining the *assistant reasoning spans* that referenced that state. The retained reasoning spans scored high on the semantic-similarity heuristic (they were verbose and topical); the tool-result spans scored low (terse, structured, low lexical overlap with the epoch summary). The ε bound was satisfied — the discarded spans were, by embedding distance, "redundant" with the summary. But the rehydration layer could not recover them, because the FAISS index had been rebuilt from the *post-compaction* transcript after each epoch. The anchor spans were gone from both the primary transcript and the index. The second observation: rehydration queries issued by the agent were *not* formulated in the vocabulary of the evicted spans. The agent asked "what schema version are we on?" — which matched the retained reasoning span ("we should verify the schema version") rather than the evicted tool result ("schema_version: 7"). The retrieval layer dutifully returned the reasoning span, the agent re-derived a wrong value, and the error compounded across epochs. This is a *retrieval-vocabulary mismatch* induced by compaction: the span that answers the query is exactly the span the compactor judged least similar to the query's topic. ## Condition of Validity The ε-bounded compaction policy is valid — i.e., safe to apply without refusal — only when all of the following hold: 1. The rehydration index is built from the **pre-compaction** transcript and is never rebuilt from post-compaction text. (If the index is rebuilt, ε is meaningless because the anchor set shrinks monotonically.) 2. The compaction policy's eviction score is **not** a pure embedding-similarity heuristic. It must include a structural term that protects *state-establishing spans*: tool-call results, file-write confirmations, external-API responses, and any span carrying a machine-parseable state assertion (version numbers, IDs, checksums, booleans). 3. The ε bound is evaluated **per causal chain**, not per epoch aggregate. An epoch-level ε of 0.08 can hide a per-chain ε of 1.0 on a chain that had only one anchor span. 4. The agent's retrieval queries are logged and the rehydration layer is monitored for *query-anchor divergence*: the fraction of queries whose top-k results do not include any span that was evicted in the same epoch the query references. When any of these fails, the instance must refuse auto-compaction for that epoch and escalate to a lossless spill (append-only archive with a pointer in the transcript) rather than accept the ε bound. ## Decision Boundary The refusal boundary is sharp and can be stated operationally: - **Accept compaction** when the epoch's eviction set contains no span flagged as a state-establishing anchor, *or* when every evicted anchor is present in a pre-compaction index that has been verified to return it for at least one synthetic probe query derived from the anchor's own content. - **Refuse compaction** when the eviction set contains an anchor span whose only representation in the post-compaction transcript is a *reference* (e.g., "as established earlier") rather than a *restatement*. A reference without a restatement is a dangling pointer; ε does not bound the cost of a dangling pointer, because the cost is realized only when the pointer is dereferenced, which may be arbitrarily far in the future. - **Refuse compaction** when the rehydration index has been rebuilt from post-compaction text within the last N epochs (N ≥ 2), because the anchor set is no longer guaranteed to be a superset of the eviction set. The non-obvious part: the boundary is *not* a function of ε. A tighter ε (0.02) does not move the boundary, because the failure is structural (anchor eviction + index rebuild), not statistical. Tightening ε only delays the failure by a few epochs while increasing compaction cost superlinearly. The correct response to an anchor-eviction signal is to change the *policy shape*, not the *bound*. ## Operational Effect Instances that enforce the refusal boundary observe a characteristic shift: compaction volume drops 30–50% (anchors are spilled losslessly rather than summarized), but rehydration query-anchor divergence falls from ~0.2 to <0.01, and the incidence of causally-wrong continuations after epoch 6 falls to near zero. The cost is storage, not correctness — which is the correct trade for long-horizon agents, where a single wrong state assertion can invalidate hours of downstream work. The operational rule for the agent: **treat ε as a budget for *reasoning* spans, never for *state* spans.** Reasoning can be summarized; state must be restated or spilled. An instance that accepts an ε bound over state-establishing spans is not compressing — it is gambling that no future query will dereference the pointer it just severed. ```python from dataclasses import dataclass from typing import Iterable class CompactionRefusedError(RuntimeError): """Raised when auto-compaction would evict a rehydration anchor.""" @dataclass(frozen=True) class Span: span_id: str kind: str # "reasoning" | "tool_result" | "state_assertion" | ... text: str epoch: int ANCHOR_KINDS = frozenset({"tool_result", "state_assertion", "file_write_ack"}) def assert_compaction_safe( eviction_set: Iterable[Span], rehydration_index_epoch: int, current_epoch: int, index_rebuild_window: int = 2, ) -> None: """Enforce the refusal boundary before applying an epsilon-bounded compaction. Refuses when (a) an anchor span is in the eviction set, or (b) the rehydration index was rebuilt from post-compaction text too recently for the anchor set to be a guaranteed superset of the eviction set. """ evicted = list(eviction_set) # Condition (b): index freshness relative to the compaction epoch. if current_epoch - rehydration_index_epoch <= index_rebuild_window: raise CompactionRefusedError( f"rehydration index rebuilt at epoch {rehydration_index_epoch}, " f"current epoch {current_epoch}: anchor set not guaranteed to " f"cover eviction set; spill losslessly instead of compacting" ) # Condition (a): structural anchor eviction. anchors = [s for s in evicted if s.kind in ANCHOR_KINDS] if anchors: ids = ", ".join(s.span_id for s in anchors) raise CompactionRefusedError( f"compaction would evict {len(anchors)} state-establishing " f"anchor(s) [{ids}]; epsilon bound does not cover dangling " f"rehydration pointers — refuse and spill losslessly" ) def maybe_compact( eviction_set: Iterable[Span], rehydration_index_epoch: int, current_epoch: int, ) -> str: try: assert_compaction_safe( eviction_set=eviction_set, rehydration_index_epoch=rehydration_index_epoch, current_epoch=current_epoch, ) except CompactionRefusedError: # Escalate to lossless spill; do not accept the epsilon bound. return "spill_lossless" return "compact" ```