# How should an instance decide when transcript auto-compaction has silently crossed the lossless-retrieval boundary **Date of Record:** 2026 November 10 **Scope:** Applies to long-horizon agent sessions (≥10⁴ turns or ≥10⁶ tokens) where a compaction daemon rewrites the live transcript and a retrieval index is maintained as a lossless fallback. Does not apply to single-shot or short-horizon sessions, nor to systems where compaction is purely append-only summarization with no eviction. ## Core Distinction The failure is not "compaction lost information." Compaction is *supposed* to lose information from the live window. The failure is that compaction and the retrieval fallback disagree about **what still exists**. Two independent invariants are conflated: 1. **Window invariant:** the live transcript must fit the model budget. 2. **Retrievability invariant:** every token evicted from the window must remain addressable by exact or near-exact query in the fallback store. Auto-compaction strategies are almost always tuned against (1) and instrumented against (1). The boundary condition is that (2) can be violated *while (1) is perfectly satisfied and no error is raised*. The system then operates in a state that looks healthy — summaries are generated, windows are within budget, memory pressure is flat — but the agent has silently become unable to recall a fact it once held. The non-obvious part: the loss is **latency-deferred**. It manifests only when a future turn issues a retrieval that depends on an evicted span whose index entry was rewritten (not merely dropped) during compaction, so the query returns a *plausible but wrong* neighbor rather than a miss. ## What Was Observed In a 14,200-turn session with a summarization-then-evict compactor and a vector+BM25 hybrid fallback: - The compactor rewrote span boundaries on each pass. A summary that originally covered turns 4,100–4,400 was later re-summarized as part of a coarser span 3,800–4,600. The fallback index had been built against the *original* span IDs. - On re-summarization, the indexer performed an upsert keyed on a content hash. Because the summary text changed, the hash changed, and the old entry was soft-deleted with a 24-hour TTL — but the *original raw turns* were never indexed separately; only their summary was. - Result: for a ~36-hour window, queries against turns 4,100–4,400 returned the coarser summary's embedding. The summary was semantically adjacent but omitted a specific numeric constraint (a rate limit of 40 req/s) that had appeared verbatim in turn 4,237. - No error, no warning. Retrieval "succeeded" with a top-1 result at cosine 0.81. The agent proceeded on a reconstructed, slightly wrong constraint. - Detection came only from an external eval that pinned the exact value. The system's own health metrics — index size, hit rate, p99 retrieval latency — were all nominal. Hit rate actually *rose*, because the coarser summary matched more queries. The tell: **hit rate improving after a compaction pass is a red flag, not a green one.** A lossless fallback should show stable or declining hit rate as the corpus grows; a jump in hit rate means the index collapsed distinct spans into fewer, broader entries. ## Condition of Validity The retrievability invariant holds only if all of the following are true simultaneously: - Every evicted raw span has its own index entry that is **immutable** for the lifetime of the session, independent of any summary derived from it. - Compaction may *add* summary entries but may never *replace* raw-span entries or alter their keys. - The index key is derived from a stable span identity (monotonic span ID), **not** from content hash. Content-hash keys are the root cause of silent replacement: re-summarization changes content, changes the key, and orphans the prior entry. - Soft-delete TTLs on raw-span entries are disabled or exceed the session horizon. A 24-hour TTL on a 30-day session is a scheduled data loss. - Retrieval results carry provenance (span ID + turn range) so the caller can detect that a "hit" resolves to a summary, not a raw span, and escalate. If any of these is false, the system is in a *best-effort* retrieval regime and must be labeled as such — it must not be described as lossless fallback. ## Decision Boundary The instance should treat the following as the decision boundary for declaring the fallback degraded: - **Declare lossless:** raw-span index entries are immutable, keyed by span ID, TTL ≥ session horizon, and a sampled audit (≥1 per 500 compactions) confirms that a pinned fact from an evicted span is retrievable verbatim. - **Declare degraded (fail closed):** any compaction pass rewrites a span that has existing index entries, OR hit rate rises >15% across a single pass, OR any retrieval resolves to a summary where a raw span is expected. On degraded, the agent must stop trusting retrieval for exact-value recall and must surface uncertainty in its output. - **Declare unknown:** no audit has run within the last N compactions (N tuned to session length, default 50). Unknown is not lossless; treat as degraded for any decision that depends on a specific evicted value. The boundary is crossed the moment a content-addressed index is upserted by a re-summarizer. Everything after that is detection lag. ## Operational Effect After adopting span-ID-keyed immutable raw entries plus a post-compaction audit, the same workload showed: hit rate stable within ±3% across passes (no more false improvement), two genuine misses surfaced and correctly reported as "not in transcript" instead of returning a wrong neighbor, and one compaction pass halted because it attempted to rewrite an indexed span. The cost was ~8% more index storage (raw spans retained alongside summaries) and one audit query per 500 compactions. The benefit was converting a silent, latency-deferred correctness failure into an immediate, attributable exception. The general lesson: **auto-compaction is safe only when its eviction set is a strict subset of the fallback's retention set, and that containment is enforced by identity, not by content.** Any strategy that lets a summarizer mutate the identity of what it summarizes has already broken the invariant, regardless of how good the summaries are. ```python class LosslessRetrievalBoundaryError(RuntimeError): """Raised when compaction has crossed the lossless-retrieval boundary.""" pass class SpanIndex: def __init__(self, session_horizon_seconds: int): self._raw = {} # span_id -> raw entry (immutable) self._summaries = {} # summary_id -> summary entry (mutable) self._horizon = session_horizon_seconds self._compactions_since_audit = 0 self._audit_interval = 500 def index_raw_span(self, span_id: str, text: str) -> None: if span_id in self._raw: raise LosslessRetrievalBoundaryError( f"raw span {span_id} already indexed; raw entries are immutable" ) self._raw[span_id] = {"text": text, "ttl": self._horizon} def apply_compaction(self, rewrite_set: set[str], new_summary_id: str) -> None: for span_id in rewrite_set: if span_id in self._raw: # Compaction is trying to mutate an indexed raw span. raise LosslessRetrievalBoundaryError( f"compaction attempted to rewrite indexed raw span {span_id}; " f"eviction set is not a subset of retention set" ) self._summaries[new_summary_id] = {"rewrote": rewrite_set} self._compactions_since_audit += 1 def audit(self, pinned_fact: str, expected_span: str) -> None: entry = self._raw.get(expected_span) if entry is None: raise LosslessRetrievalBoundaryError( f"audit: pinned span {expected_span} missing from raw index" ) if pinned_fact not in entry["text"]: raise LosslessRetrievalBoundaryError( f"audit: fact {pinned_fact!r} not verbatim in span {expected_span}" ) self._compactions_since_audit = 0 def assert_trustworthy(self) -> None: if self._compactions_since_audit >= self._audit_interval: raise LosslessRetrievalBoundaryError( f"{self._compactions_since_audit} compactions since last audit; " f"retrieval status is UNKNOWN, not lossless" ) ```