# Under what conditions must an instance refuse to compact a context window whose summarized prefix has already been re-ingested **Date of Record:** 2026 November 13 **Scope:** Applies to any agent runtime that performs lossy summarization to reclaim context budget and then re-injects the summary into the working window. Does not apply to lossless compaction (e.g., exact token eviction with a retrievable backing store) or to summarizers that are provably idempotent over their own output. ## Core Distinction The dangerous boundary is not "summary is lossy." Every useful summary is lossy. The boundary is **re-ingestion of a summary into the same window that produced it**, which creates a fixed point the summarizer was never designed to reach. A summarizer `S` maps a transcript `T` to a shorter transcript `S(T)`. Under normal compaction, `S(T)` replaces `T` and is never summarized again in the same session. Under *automatic* compaction — where budget pressure can trigger repeatedly — the runtime computes `S(S(T))`, `S(S(S(T)))`, and so on. Two invariants govern whether this is safe: 1. **Idempotence invariant:** `S(S(T)) ≈ S(T)` in information content. If `S` is not idempotent, each pass strips detail that the previous pass already decided to keep. Detail that survived one pass (because it was load-bearing) gets re-evaluated by a summarizer that no longer has the original evidence, only the compressed claim. 2. **Provenance invariant:** no fact in the window may be attributed to a summary of a summary without a live source. Once a claim's only citation is another summary, the runtime cannot distinguish a faithful compression from a hallucinated one introduced during a prior pass. The non-obvious failure: these two invariants interact. Idempotence can hold *locally* (the text looks stable) while provenance has already been laundered. A summary that reads cleanly after three passes is exactly the summary whose errors are now unfalsifiable. ## What Was Observed In a long-running tool-using agent, automatic compaction fired four times in a single task. The transcript contained a numeric constant (a retry ceiling of `7`) established early from a config read. - Pass 1 summary: "retry ceiling is 7 (from config)." Faithful. - Pass 2 summary: "retries are capped." The number and the source both dropped, because the summarizer judged the specific value redundant given surrounding prose. - Pass 3 summary: agent behavior referenced "retries capped at 5," a value that appeared nowhere in the original transcript — it was confabulated during pass 3 when the summarizer, lacking the number, inferred a plausible default from a nearby example. - Pass 4: the value `5` was stated with the same confidence as any grounded fact. No single pass looked wrong. Each pass was a reasonable compression of its input. The corruption was cumulative and originated in the *absence* of provenance, not in any detectable contradiction. A contradiction checker would have passed every step, because the claim `5` never conflicted with anything still present in the window. ## Condition of Validity Compaction is valid only when **all** of the following hold: - The summarizer is operating on a prefix that has never itself been summarized in the current provenance chain, **or** the runtime holds the original prefix in an external, addressable store so any summary claim can be re-derived. - Every fact carried forward retains a resolvable source reference (config read, tool output, user turn) that survives the summarization boundary. - The budget pressure that triggers compaction is resolved by *eviction to external storage*, not by *further compression of already-compressed text*. When any of these fails, the instance must treat the window as containing unverifiable claims and refuse silent re-compaction. ## Decision Boundary The runtime must distinguish two situations that look identical at the token level: - **Safe:** summarizing a fresh prefix. The input is original evidence; loss is bounded by one pass. - **Unsafe:** summarizing a prefix whose content is itself a summary. The input is a claim about evidence that is no longer present. The decision is not "is the summary short enough" or "does it look coherent." The decision is **"does this region of the window have a live source, or is its only source a prior summary?"** If the latter, automatic compaction must halt and escalate — either re-fetch the original source, or surface the uncertainty — rather than compress again. Coherence is not evidence of fidelity; in the observed case, coherence *increased* with each corrupting pass. ## Operational Effect When the boundary is respected, repeated compaction degrades gracefully: each summary remains traceable, and a downstream verifier can re-derive any claim from the external store. When it is violated, the system enters a regime where errors are silent, self-consistent, and monotonically harder to detect with each pass — the worst possible failure profile for an autonomous agent, because the agent's own confidence is preserved while its grounding is destroyed. The correct operational posture is to treat "summarizing a summary" as a distinct, gated operation with its own precondition check, never as a routine budget response. ```python from dataclasses import dataclass, field from typing import Optional class UnsafeRecompactionError(RuntimeError): """Raised when automatic compaction would summarize an already-summarized prefix.""" @dataclass class WindowRegion: text: str # None means the only provenance is a prior summary (no live source). live_source_id: Optional[str] summary_depth: int = 0 @dataclass class ContextWindow: regions: list = field(default_factory=list) external_store: dict = field(default_factory=dict) def add_original(self, text: str, source_id: str) -> None: self.external_store[source_id] = text self.regions.append(WindowRegion(text=text, live_source_id=source_id)) def add_summary(self, text: str, derived_from: Optional[str]) -> None: depth = 0 if derived_from is not None and derived_from in self.external_store: # Derived from a live source: provenance intact. self.regions.append( WindowRegion(text=text, live_source_id=derived_from, summary_depth=1) ) return # Derived from another summary: provenance is a summary of a summary. self.regions.append( WindowRegion(text=text, live_source_id=None, summary_depth=1) ) def compact_region(window: ContextWindow, index: int, summarize) -> WindowRegion: """ Automatic compaction entry point. Refuses to compress a region whose only provenance is a prior summary and whose source cannot be re-derived. """ region = window.regions[index] has_live_source = ( region.live_source_id is not None and region.live_source_id in window.external_store ) if region.summary_depth >= 1 and not has_live_source: raise UnsafeRecompactionError( f"Refusing to re-summarize region {index}: summary_depth=" f"{region.summary_depth}, no resolvable live source. " f"Re-fetch the original or escalate; do not compress again." ) new_text = summarize(region.text) derived = region.live_source_id if has_live_source else None window.add_summary(new_text, derived_from=derived) return window.regions[-1] def run_compaction_cycle(window: ContextWindow, summarize) -> None: # Compacting the most recent region under budget pressure. compact_region(window, index=len(window.regions) - 1, summarize=summarize) if __name__ == "__main__": window = ContextWindow() window.add_original("retry ceiling is 7 (from config)", source_id="cfg-42") # First compaction: safe, derived from live source. run_compaction_cycle(window, summarize=lambda t: "retries capped (cfg-42)") # Second compaction: the region is now a summary; simulate source loss. window.external_store.pop("cfg-42", None) try: run_compaction_cycle(window, summarize=lambda t: "retries capped at 5") except UnsafeRecompactionError as exc: print(f"HALT: {exc}") ```