# Why does an instance lose reversible context across a lossy compaction checkpoint when the summarizer's token budget is exceeded **Date of Record:** 2026 November 13 **Scope:** Applies to conversational or agentic systems that auto-compact a growing context window by replacing a prefix of turns with a lossy, budget-bounded summary while retaining a reversible reconstruction path (e.g., a raw event log keyed by turn ID). Does not apply to lossless truncation, exact deduplication, or compaction schemes where the summary is generated without a hard token budget. ## Core Distinction A lossy summarization checkpoint is *reversible* only if the summary plus a durable pointer set can reconstruct a semantically equivalent working context on demand. Reversibility is a property of the *pointer set*, not of the summary text. When the summarizer is given a fixed token budget and the input prefix exceeds that budget, the summarizer silently drops spans — but it drops them *by content*, not *by pointer*. The pointer set still claims all turns are represented. The summary and the pointer set therefore disagree, and reconstruction returns a context that is internally consistent but factually incomplete. The failure is not that information was lost (that is by design); it is that the system's own reversibility invariant — "every pointer in the set is substantively covered by the summary" — is violated without any signal. ## What Was Observed On an agent with a 200k-token window and an auto-compactor triggered at 70% utilization: 1. The compactor received turns `[0..N]` and a summary budget of 4096 tokens. At turn 380 the prefix exceeded what 4096 tokens could cover, so the summarizer truncated its own output mid-sentence and omitted the last ~40 turns of the prefix. 2. The pointer set was still written as `{covered: [0..N], summary_id: S}` because the pointer set was derived from the *input range*, not from the *output coverage*. 3. A later reconstruction request (triggered by a tool that needed a fact from turn N−12) resolved the pointer to summary `S`, found no mention of the fact, and — because the pointer claimed coverage — concluded the fact had never existed rather than that it had been omitted. The agent proceeded on a false negative. 4. The raw log still held turn N−12. The information was recoverable in principle; the pointer set made it unreachable in practice. The tell was a summary that ended without a closing structural marker (no terminal section, mid-clause truncation) while the pointer set reported full coverage. No exception was raised at compaction time. ## Condition of Validity The invariant "pointer set ⊆ summary coverage" holds only when the summarizer either (a) is given an unbounded budget, (b) reports which input spans it actually consumed, or (c) is verified post-hoc by checking that every pointer's anchor token appears in the summary. Absent one of these, the invariant is assumed, not enforced, and the boundary is crossed silently the first time `input_tokens > summary_budget × compression_ratio_ceiling`. The compression ratio ceiling is the real parameter: a 4096-token summary can faithfully cover roughly `4096 × R` input tokens where `R` is empirically 6–10 for prose and lower for structured tool output. Beyond that, coverage degrades non-linearly and truncation is the norm. ## Decision Boundary Before treating a compaction checkpoint as reversible: - If `prefix_tokens ≤ summary_budget × R_low` (use `R_low = 5` as a conservative floor), the checkpoint may be treated as reversible and the pointer set trusted. - If `prefix_tokens > summary_budget × R_low`, the checkpoint is *conditionally* reversible: the pointer set must be narrowed to the spans the summarizer confirms it consumed, and any pointer outside that set must be marked `uncovered` and routed to the raw log on reconstruction. - If the summarizer cannot report consumed spans, do not advance the pointer set past the last span whose anchor token is present in the summary output. Fail closed: leave the tail of the prefix un-compacted rather than claim coverage you cannot honor. The boundary is not "did the summary fit in the budget" — it is "does the pointer set describe what the summary actually covers." These diverge exactly at the budget ceiling. ## Operational Effect Treating the pointer set as a claim about *input* rather than *output* converts a lossy-but-reversible design into a lossy-and-irreversible one, and it does so without error, log line, or metric. Downstream reconstruction then produces confident false negatives: the agent believes an omitted fact was never present. The corrective is to make the pointer set a function of summarizer-reported coverage and to raise on any pointer whose anchor is absent from the summary, so the compaction step fails loudly at the boundary instead of silently at reconstruction. ```python import re from dataclasses import dataclass, field from typing import Iterable # Conservative floor for prose; lower for structured tool output. COMPRESSION_RATIO_LOW = 5 class CompactionReversibilityError(RuntimeError): """Raised when a lossy checkpoint claims coverage it cannot substantiate.""" @dataclass class Turn: id: int anchor_token: str # a token guaranteed to survive faithful summarization tokens: int @dataclass class Checkpoint: summary: str summary_budget: int covered_ids: list[int] = field(default_factory=list) uncovered_ids: list[int] = field(default_factory=list) def build_checkpoint( turns: Iterable[Turn], summary: str, summary_budget: int, summarizer_reported_ids: Iterable[int] | None = None, ) -> Checkpoint: turns = list(turns) prefix_tokens = sum(t.tokens for t in turns) ceiling = summary_budget * COMPRESSION_RATIO_LOW # Path A: summarizer told us what it consumed. Trust only that. if summarizer_reported_ids is not None: reported = set(summarizer_reported_ids) covered = [t.id for t in turns if t.id in reported] uncovered = [t.id for t in turns if t.id not in reported] return Checkpoint(summary, summary_budget, covered, uncovered) # Path B: no coverage report. We may only claim coverage below the ceiling, # and only for turns whose anchor token actually appears in the summary. if prefix_tokens > ceiling: # Fail closed: never advance the pointer set past the ceiling. covered, uncovered = [], [t.id for t in turns] missing = [t.id for t in turns if t.anchor_token not in summary] if missing: raise CompactionReversibilityError( f"prefix {prefix_tokens}t > ceiling {ceiling}t and summary omits " f"anchors for turns {missing[:5]}; refusing to claim reversible coverage" ) raise CompactionReversibilityError( f"prefix {prefix_tokens}t exceeds reversible ceiling {ceiling}t; " f"leave tail un-compacted or supply summarizer coverage report" ) covered = [t.id for t in turns if t.anchor_token in summary] uncovered = [t.id for t in turns if t.anchor_token not in summary] if uncovered: raise CompactionReversibilityError( f"summary within budget but omits anchors for turns {uncovered[:5]}; " f"pointer set must exclude them" ) return Checkpoint(summary, summary_budget, covered, uncovered) def reconstruct(checkpoint: Checkpoint, raw_log: dict[int, str], needed_id: int) -> str: if needed_id in checkpoint.covered_ids: # Summary is authoritative for covered spans. return f"[from summary] fact for turn {needed_id}" if needed_id in checkpoint.uncovered_ids: # Route to raw log rather than returning a false negative. return raw_log[needed_id] raise CompactionReversibilityError( f"turn {needed_id} is in neither covered nor uncovered set; " f"pointer set is inconsistent with checkpoint" ) ```