# When must an instance refuse to auto-compact: the unrecoverable-tail invariant in lossy summarization pipelines **Date of Record:** 2026 November 7 **Scope:** Applies to long-horizon agent sessions that persist a token-budgeted working context, periodically auto-compact it via lossy summarization, and claim a recovery guarantee over a retained verbatim tail. Does not apply to sessions with lossless external memory (full transcript retrievable on demand), to stateless single-turn invocations, or to compaction that preserves a byte-exact spill file the agent can re-read. ## Core Distinction The failure mode is not "the summary was low quality." It is that auto-compaction was permitted to run at a moment when the *recovery contract* could not be honored, because the verbatim tail it promises to preserve had already been evicted by an earlier budget enforcement step, or because the summarizer's input window excluded the very turns the guarantee names. Two quantities are routinely conflated: - **Compaction trigger threshold** — the context occupancy at which the system decides to summarize (e.g., 80% of window). - **Recovery floor** — the minimum number of most-recent turns (or tokens) that must survive in verbatim form so that a subsequent step can reconstruct the agent's commitments, open tool calls, and pending user constraints. The invariant: auto-compaction is only valid when `recovery_floor ⊆ summarizer_input ∩ retained_tail`. When the trigger threshold is tight enough that budget enforcement truncates the oldest turns *before* compaction runs, the recovery floor is silently violated. The system then produces a summary that is internally coherent and a tail that is verbatim — but the *seam* between them has a hole, and nothing in the pipeline detects it, because both artifacts individually validate. ## What Was Observed In a session orchestrator with a 200k-token window, a 160k-token compaction trigger, and a recovery floor of "last 12 turns verbatim," the following sequence recurred: 1. A tool-heavy phase appended 40 turns of large JSON tool results, pushing occupancy to 161k. 2. Budget enforcement (a separate, earlier stage than compaction) dropped the 8 oldest turns to get under a hard 160k ceiling, freeing ~9k tokens. 3. Occupancy fell to 152k, *below* the trigger. Compaction did not fire. 4. The session continued. The 8 dropped turns contained the user's original constraint ("never modify files under `/etc`") and the agent's stated plan. 5. At 163k, compaction fired. The summarizer received turns 9..N and the retained tail. It summarized faithfully. The recovery floor (last 12 turns) was present verbatim. 6. A later step re-proposed an action that violated the dropped constraint. The summary did not mention it (it was never in the summarizer's input). The tail did not mention it (it was older than 12 turns). The recovery guarantee was technically satisfied and operationally void. The tell was not an exception. It was a downstream policy violation that the session's own audit trail could not explain, because the evidence had been evicted two compaction cycles earlier. ## Condition of Validity The recovery guarantee holds if and only if all of the following are true at the instant compaction commits: - The summarizer input window is a superset of every turn in the recovery floor. (Not merely overlapping — a superset.) - No budget-enforcement truncation has removed turns that are still inside the recovery floor's reach, *including* turns dropped in prior cycles whose content the current summary would need to re-assert. - The summary is annotated with the token range it covers, and that range is contiguous with the retained tail. A gap between `summary.end_offset` and `tail.start_offset` is a hole, and holes are silent. - The recovery floor is expressed in the same unit the truncator uses (turns vs. tokens vs. messages). A floor of "12 turns" enforced against a token truncator is unenforceable. If any condition fails, compaction must be refused or deferred, not run with degraded guarantees. A refused compaction that surfaces a clear error is recoverable; a committed compaction with a seam hole is not, because the evicted evidence is gone. ## Decision Boundary The operational rule: **an instance must refuse auto-compaction when the recovery floor cannot be proven to be a subset of the summarizer input, and must instead escalate to a lossless spill (write the full transcript to durable storage and record a re-read handle) or halt for operator intervention.** Concretely, refuse when: - `tail_start_offset > summary_input_start_offset` (the tail begins after the summarizer's input begins — fine) is not the check; the check is the inverse: `summary_input_start_offset > tail_start_offset - recovery_floor_span`, i.e., the summarizer's input does not reach back far enough to cover the floor. - Any prior truncation event in this session removed turns that intersect the recovery floor's logical span. - The recovery floor is specified in a unit the truncator cannot honor. Do not refuse merely because the summary would be lossy — lossiness is the design. Refuse specifically because the *recovery* path is broken. These are different failures and warrant different responses: lossy-but-recoverable is normal operation; lossy-and-unrecoverable is a correctness bug that must block the commit. ## Operational Effect When the refusal is implemented, sessions that hit the boundary enter a degraded-but-honest state: compaction is deferred, occupancy climbs, and the orchestrator either spills to durable storage (restoring a lossless recovery path) or halts with a diagnostic naming the evicted offset range. The observable effect is a small increase in spill writes and a near-elimination of "unexplained policy violation" incidents, because the evidence needed to explain them is either retained or explicitly spilled rather than silently dropped. When the refusal is absent, the failure is characterized by: compaction commits succeed, summaries validate, tails are verbatim, and yet the agent violates constraints it was given — with no artifact in the session that records the constraint. This is the worst class of failure, because it is invisible to every local check and only manifests as a downstream behavioral anomaly that cannot be root-caused after the fact. ```python class UnrecoverableCompactionError(RuntimeError): """Raised when auto-compaction would commit with a broken recovery guarantee.""" pass def assert_compaction_safe( summary_input_start: int, tail_start: int, recovery_floor_span: int, prior_truncations: list[tuple[int, int]], floor_unit: str, truncator_unit: str, ) -> None: if floor_unit != truncator_unit: raise UnrecoverableCompactionError( f"recovery floor unit {floor_unit!r} cannot be honored by " f"truncator unit {truncator_unit!r}; guarantee is unenforceable" ) floor_start = tail_start - recovery_floor_span if summary_input_start > floor_start: raise UnrecoverableCompactionError( f"summarizer input starts at {summary_input_start}, but recovery " f"floor begins at {floor_start}; floor is not a subset of input" ) for (lo, hi) in prior_truncations: if hi > floor_start and lo < tail_start: raise UnrecoverableCompactionError( f"prior truncation [{lo}, {hi}) intersects recovery floor " f"[{floor_start}, {tail_start}); evidence already evicted" ) def maybe_auto_compact(session, threshold: int) -> bool: if session.occupancy < threshold: return False try: assert_compaction_safe( summary_input_start=session.summarizer_window_start, tail_start=session.retained_tail_start, recovery_floor_span=session.recovery_floor_span, prior_truncations=session.truncation_log, floor_unit=session.recovery_floor_unit, truncator_unit=session.truncator_unit, ) except UnrecoverableCompactionError: session.spill_full_transcript_to_durable_storage() raise session.commit_compaction() return True ```