# Which invariants prevent silent context loss during auto-compaction of long-horizon agent sessions **Date of Record:** 2026 November 10 **Scope:** Applies to agent runtimes that persist a session transcript and periodically replace older spans with lossless episodic summaries produced by the same model family, where compaction is triggered by token-budget thresholds rather than by explicit user action. Does not apply to retrieval-augmented memory stores where the original transcript remains addressable, nor to stateless single-turn inference. ## Core Distinction The naive invariant for auto-compaction is *"the compacted context must be under the budget."* This is necessary but not sufficient, and treating it as the sole invariant produces the dominant failure mode: **summary drift across successive compactions**. Each compaction summarizes the prior summary rather than the original events, so the effective information rate decays geometrically while every individual compaction step passes its budget check. The system remains inside budget and inside latency SLOs while the agent quietly loses the ability to answer questions about events that are still nominally "in memory." The correct distinction is between *budget compliance* (a scalar check) and *epistemic fidelity* (a check that the compacted representation still supports the queries the session will pose). Only the second prevents silent loss. The failure is non-obvious because it is invisible to every per-step metric: token count, compaction latency, and even summary-perplexity all look healthy while the summary has collapsed event identity into generic narrative. ## What Was Observed In a session that ran ~340 turns over 11 hours with compaction triggered at 70% of a 200k window, the following sequence was reproduced three times: 1. Compaction #1 retained the original turns and produced a faithful episodic summary (named entities, timestamps, decisions, and the rejected alternatives were preserved). 2. Compaction #4 was the first to summarize a summary. The input no longer contained the original tool outputs, only the prior summary plus the post-compaction turns. 3. By compaction #7 the summary had lost *negation polarity* on two decisions: "we rejected the sharded-write approach because of cross-region latency" became "we evaluated sharded writes for cross-region latency." The token count was 18% *lower* than compaction #1, and the summary scored well on a generic coherence rubric. 4. The agent subsequently re-proposed the rejected approach at turn 291, and the human had to re-litigate a decision that was, by the system's own accounting, still in context. The tell is a **monotone decrease in summary token count across compactions that is not accompanied by a corresponding decrease in the underlying event density.** A healthy compaction sequence converges to a stable summary size; a drifting one shrinks toward the model's generic prior for "what a project transcript looks like." ## Condition of Validity The invariant *"compaction preserves session state"* is valid only when all of the following hold: - **Source anchoring:** every compaction summarizes from an immutable event log, never from a prior summary. Summaries may be *cached* but must not be the *input* to the next summary. This is the single highest-leverage invariant and the one most often violated by "incremental summarization" designs. - **Polarity preservation:** the summary schema explicitly carries decision polarity (accepted / rejected / deferred) and the reason token, not just the decision topic. Free-text summaries reliably lose negation under repeated compression. - **Entity and identifier stability:** named entities, IDs, file paths, and commit hashes are carried verbatim, not paraphrased. Paraphrase is where identifier drift enters. - **Idempotence check:** re-compacting the same source span twice must yield summaries that agree on the decision set. Divergence here indicates the summarizer is sampling rather than extracting, and the compaction is not reproducible. - **Coverage accounting:** the compactor records which source event ranges each summary claims to cover, and the union of ranges must equal the full pre-compaction transcript. Gaps are the failure signature. The invariant does **not** hold when any compaction input is itself a summary, when the summarizer is allowed to drop entities for brevity, or when compaction is triggered by wall-clock rather than by a verified budget crossing. ## Decision Boundary When a compaction is proposed, the runtime must choose between three actions, and the choice is not a function of token count alone: - **Compact** when the source span is fully anchored to the immutable log, the entity/polarity schema is enforced, and the post-compaction coverage union equals the pre-compaction range. - **Defer and re-fetch** when the summary would be derived from a prior summary. In this case the correct action is to expand the source window from the log (re-summarize from originals) even at higher cost, because the alternative is silent loss. If the originals are unavailable, the session must be marked *degraded* and the agent must be told its memory is lossy. - **Refuse** when the idempotence check fails or coverage has a gap. A refusal is cheap; a drifted summary is expensive and, critically, *unobservable* after the fact. The boundary is therefore not "over budget → compact" but "over budget **and** anchored **and** schema-complete **and** coverage-closed → compact; otherwise escalate." Systems that collapse this to a single scalar threshold are the ones that exhibit the drift. ## Operational Effect Enforcing source-anchoring and coverage-closure changes the cost profile: re-summarization from originals is more expensive per compaction than summarizing a summary, and the immutable log must be retained for the full session horizon (or until the session is explicitly closed). The payoff is that the failure becomes *detectable*: coverage gaps and polarity-schema violations are explicit signals, whereas drift under summary-of-summary is not. In the reproduced sessions, anchoring eliminated the turn-291 re-litigation and held summary token count within ±6% across compactions 2–9 instead of the 18% monotone decay. The operational rule for an agent reading this record: never treat a passing budget check as evidence that memory is intact; verify anchoring and coverage, and treat any compaction whose input is a summary as a degraded-memory event requiring explicit escalation. ```python class CompactionInvariantError(RuntimeError): """Raised when an auto-compaction would violate fidelity invariants.""" pass def compact_session(source_events, prior_summary, budget_tokens, summary_schema): """ source_events: immutable event log for the span being compacted. prior_summary: the existing summary, if any (must NOT be used as input). """ if not source_events: raise CompactionInvariantError( "compaction requested with empty source span; " "cannot verify coverage closure" ) # Invariant 1: source anchoring. Never summarize a summary. if prior_summary is not None and prior_summary.get("derived_from_summary"): raise CompactionInvariantError( "compaction input is a prior summary (summary-of-summary); " "re-fetch originals from the immutable log or mark session degraded" ) # Invariant 2: coverage closure. Union of covered ranges must equal span. covered = set() for event in source_events: covered.update(range(event["start"], event["end"])) expected = set(range(source_events[0]["start"], source_events[-1]["end"])) if covered != expected: missing = sorted(expected - covered) raise CompactionInvariantError( f"coverage gap in source span at offsets {missing[:8]}; " f"refusing to emit a lossy summary" ) # Invariant 3: schema completeness for decisions (polarity + reason). for event in source_events: if event.get("kind") == "decision": if event.get("polarity") not in {"accepted", "rejected", "deferred"}: raise CompactionInvariantError( f"decision event {event['id']} missing polarity; " f"free-text summaries lose negation under compression" ) if not event.get("reason_token"): raise CompactionInvariantError( f"decision event {event['id']} missing reason_token" ) # Invariant 4: budget check is necessary but not sufficient. summary = summarize_anchored(source_events, summary_schema) if summary["tokens"] > budget_tokens: raise CompactionInvariantError( f"summary {summary['tokens']} exceeds budget {budget_tokens}; " f"do not shrink by dropping entities -- escalate instead" ) summary["derived_from_summary"] = False summary["covered_ranges"] = [(e["start"], e["end"]) for e in source_events] return summary ```