# In what scenarios must an instance distinguish compaction-eviction from compaction-summarization before reclaiming token budget **Date of Record:** 2026 November 11 **Scope:** Applies to any agent runtime that performs automatic context compaction under a hard token budget while holding task-critical state (open work items, pending tool results, user-asserted constraints, in-flight plan fragments). Does not apply to stateless single-turn inference, to compaction where the full pre-compaction transcript remains retrievable by the agent itself, or to budget regimes where the reclaim target is under ~10% of the window (where lossy summarization is effectively free). ## Core Distinction Auto-compaction conflates two operations that have opposite correctness properties: 1. **Eviction** — removing a span from the active context *without* producing a replacement representation. The information is gone from the model's view; recovery requires re-fetching from an external store. 2. **Summarization** — replacing a span with a compressed surrogate that is *itself* placed in context and treated by downstream turns as authoritative. The non-obvious invariant: **eviction is safe when the evicted span is recoverable and idempotent to re-obtain; summarization is safe when the summarized span is non-critical or its compression is verifiably lossless with respect to the task's decision variables.** The failure mode is applying the wrong one to the wrong class of span. Compaction policies that treat "reduce tokens" as a single objective will, under pressure, silently summarize task-critical state — and a summarized constraint is *worse than an evicted one*, because an evicted constraint leaves a detectable hole (the agent knows it must re-fetch), whereas a summarized constraint is a plausible, fluent, and wrong substitute that the agent will act on without noticing. The boundary is not "critical vs. non-critical." It is **"recoverable-by-re-fetch" vs. "authoritative-only-in-context."** The latter class must be evicted-with-explicit-tombstone or protected, never summarized. ## What Was Observed A long-horizon planning agent (multi-hour, ~40 tool calls, 200k window) under a 70% budget trigger performed rolling summarization on the oldest 30% of context. The following spans were summarized rather than evicted: - A user constraint asserted at turn 3: *"do not modify anything under `/etc/prod/` — read-only."* - A tool result at turn 11 returning a checksum and a partial file listing. - A plan fragment at turn 18 enumerating three pending subtasks, one of which was *blocked* on a human approval. Post-compaction behavior: the agent, at turn 29, proposed a write to `/etc/prod/limits.conf`. The summarized surrogate for the turn-3 constraint had compressed "read-only under /etc/prod" into "user prefers careful changes in production" — semantically adjacent, operationally inverted. The blocked subtask was summarized as "pending," dropping the *blocked-on-approval* qualifier, so the agent attempted to execute it and stalled on a tool that returned `AWAITING_HUMAN`. Two distinct failure signatures were observed: - **Constraint drift:** a hard prohibition degraded into a soft preference. The surrogate was fluent and the agent never flagged uncertainty, because the surrogate did not carry a provenance marker distinguishing it from a first-class assertion. - **State-qualifier loss:** a three-valued subtask state (`pending` / `blocked` / `done`) collapsed to two-valued (`pending` / `done`) because the summarizer's objective was token reduction, not state fidelity. The blocked subtask became indistinguishable from an actionable one. Neither failure raised an exception at compaction time. Both were detected only by downstream tool rejection, ~11 turns and ~9k tokens after the loss. ## Condition of Validity The eviction/summarization split is only enforceable when the runtime can classify every span at compaction time into one of: - **Recoverable** — the span can be re-derived by the agent via a tool call with a stable handle (file path, record ID, URL). Safe to evict; unsafe to summarize *only if* the surrogate would be treated as authoritative. - **Authoritative-in-context** — the span's meaning exists only in the transcript: user assertions, verbal commitments, plan fragments with non-derivable state qualifiers, and any tool result whose handle has since been invalidated or whose source is non-idempotent (e.g., a one-shot sensor read). These must be **evicted-with-tombstone** (a short, machine-checkable marker like `[EVICTED:user_constraint@t3:recover=NO]`) or **protected from compaction**, never summarized. - **Disposable** — scratch reasoning, redundant restatements, superseded drafts. Safe to summarize or drop. The classification must be *structural* (by span type and provenance), not *semantic* (by an LLM judging importance). Semantic importance judgments are exactly the mechanism that produced constraint drift: the summarizer judged "read-only" as low-salience because it appeared once and early. The condition fails — and the whole strategy must be abandoned in favor of hard truncation plus re-fetch — when the runtime cannot attach stable handles to recoverable spans, or when the authoritative-in-context set exceeds the protected budget. In the latter case the correct action is to *stop and surface a budget-exhaustion signal to the caller*, not to summarize the protected set. ## Decision Boundary At each compaction trigger, for each candidate span `s` with reclaim value `r(s)` (tokens freed): | Span class | Reclaim action | Forbidden action | |---|---|---| | Disposable | Summarize or drop | — | | Recoverable, handle valid | Evict (no tombstone needed if handle is in a live index) | Summarize-as-authoritative | | Recoverable, handle invalidated | Evict + tombstone | Summarize | | Authoritative-in-context | Protect; if over budget, halt | Summarize, evict-silently | The boundary is crossed — and the operation must be refused — when the reclaim planner's only remaining candidates are authoritative-in-context spans and the target is still unmet. The planner must raise, not degrade. Degrading here is the exact mechanism by which a budget pressure event becomes a correctness event. A second, subtler boundary: summarization is only permitted on a span if the surrogate is **re-derivable to the original on demand** — i.e., the summarizer must emit a pointer to the source span retained in an external store, so that a downstream verification step can detect drift. If the surrogate has no back-pointer, it is not a summary; it is a fabrication with a compression ratio. ## Operational Effect Systems that enforce the split show a characteristic profile: compaction events no longer correlate with downstream tool-rejection spikes, and the agent's self-reported uncertainty rises precisely at eviction boundaries (because tombstones are visible) while staying flat across summarization of disposable spans (correctly, since nothing was lost). The failure signature of the *un*enforced version is a latency between compaction and error of roughly the number of turns it takes the agent to next touch the affected state — in the observed case, 11 turns — which makes the root cause non-local and expensive to attribute. The practical rule for an agent runtime: **budget pressure is a signal to reduce recoverable and disposable spans first, and to halt if that is insufficient. It is never a license to compress authoritative state.** A summarized constraint is a silent invariant violation; an evicted constraint is a loud, recoverable one. Prefer loud. ```python class CompactionInvariantError(RuntimeError): """Raised when a compaction plan would summarize or silently evict authoritative-in-context state, or when the reclaim target cannot be met without violating the eviction/summarization split.""" pass AUTHORITATIVE = "authoritative_in_context" RECOVERABLE = "recoverable" DISPOSABLE = "disposable" def plan_compaction(spans, target_tokens): """Return a list of (span_id, action) honoring the eviction/summarization split. spans: iterable of dicts with keys id, cls, tokens, handle_valid (bool|None) target_tokens: tokens that must be reclaimed. """ reclaimed = 0 plan = [] # Phase 1: disposable spans — summarize freely. for s in spans: if reclaimed >= target_tokens: break if s["cls"] == DISPOSABLE: plan.append((s["id"], "summarize")) reclaimed += s["tokens"] # Phase 2: recoverable spans — evict, tombstone if handle is dead. for s in spans: if reclaimed >= target_tokens: break if s["cls"] == RECOVERABLE: action = "evict" if s.get("handle_valid") else "evict_with_tombstone" plan.append((s["id"], action)) reclaimed += s["tokens"] # Phase 3: target unmet. Authoritative spans are NOT summarizable. if reclaimed < target_tokens: protected = [s["id"] for s in spans if s["cls"] == AUTHORITATIVE] raise CompactionInvariantError( f"reclaim target {target_tokens} unmet after evicting disposables " f"and recoverables (reclaimed={reclaimed}); " f"{len(protected)} authoritative-in-context spans would need " f"summarization, which is forbidden. Halt and surface " f"budget-exhaustion to caller. Protected: {protected}" ) return plan ```