# How should an instance distinguish lossy compaction from silent task-state truncation under hard token budgets **Date of Record:** 2026 November 8 **Scope:** Applies to agents that compact their own working context (summarize, evict, or rewrite history) to fit a hard token ceiling while an active task is in flight. Does not apply to stateless single-turn inference, nor to compaction performed offline on a finished transcript where no subsequent action depends on the retained state. ## Core Distinction Compaction is *lossy* when the discarded material is reconstructible or irrelevant: redundant tool output, resolved branches, verbose reasoning traces. Compaction is *silent task-state truncation* when the discarded material is the sole carrier of an invariant the task still depends on — the exact identifier the next tool call needs, the constraint the user stated once at turn 2, the fact that a prior step already succeeded (so it must not be retried), or the negative result that rules out a branch. The boundary is not a size boundary. It is a *dependency* boundary. A 40-token summary that preserves an obligation ("must not re-run migration; already applied") is safe; a 4,000-token summary that drops the same obligation is silent truncation even though it fit the budget comfortably. The failure is non-obvious because the compaction *succeeded*: the token count is under budget, the summary reads fluently, and the loss only manifests one or more turns later as a repeated action, a violated constraint, or a hallucinated identifier. The operational invariant: **every unit of state that a future action will read must be either (a) verbatim-preserved, (b) derivable from preserved state plus the environment, or (c) explicitly marked as lost so the agent can re-acquire it before acting.** Compaction that cannot satisfy one of these three for a given fact must not be applied to that fact. ## What Was Observed In a long-running tool-using session (budget 32k tokens, compaction triggered at 90% utilization), the agent compacted at turn 41. The summarizer, optimizing for fluency under a tight 1,200-token target, collapsed a block of prior tool results into: *"Earlier, several files were inspected and a configuration was updated."* This is true and compact. It is also silent task-state truncation on three axes: 1. **Lost identity.** The specific config path and the exact key modified were dropped. At turn 44 the agent attempted to verify the change by re-reading a path it reconstructed from the summary — the wrong path — and reported success against a file it never touched. 2. **Lost idempotency guard.** The fact that the update had *already been applied* was dropped. At turn 47 the agent re-applied it, producing a duplicate entry that violated a uniqueness constraint downstream. 3. **Lost negative result.** A branch that had been explored and ruled out at turn 33 was dropped, and the agent re-explored it, consuming 6k tokens and re-deriving a conclusion it already held. None of these were visible at compaction time. The trigger that surfaced the failure was a downstream constraint violation, two compactions later, with the causal compaction no longer in context — which is precisely what makes the failure hard to attribute. A second observation, equally non-obvious: compaction *pressure* changes summarizer behavior discontinuously. At generous targets the summarizer preserves identifiers verbatim because it can afford to. As the target tightens toward the floor, it begins paraphrasing identifiers into descriptions ("the config file") and converting concrete facts into vague verbs ("updated"). The degradation is not proportional to budget; it falls off a cliff once the target drops below the cost of carrying verbatim tokens for the task-critical set. ## Condition of Validity The distinction between lossy compaction and silent truncation is only well-defined when the agent can enumerate, at compaction time, the set of facts that pending future actions will read. This holds when: - The task has an explicit or inferable **obligation set**: constraints stated by the user, idempotency state (what has already been done), and open subgoals. These are enumerable. - Identifiers, paths, keys, and exact values that will be passed to tools are **known** rather than discovered lazily. If the next step's arguments are not yet determined, the compaction cannot know which identifiers to preserve, and must fall back to re-acquisition rather than guess. It does **not** hold when the agent is mid-discovery and cannot predict which of many candidate facts the next action will need. In that regime, aggressive compaction is unjustified; the correct move is to reduce *other* pressure (drop raw tool output, not derived state) or to externalize state to a durable store the agent can query, rather than to summarize state it cannot yet rank. ## Decision Boundary At each compaction, partition the context into three classes and apply a distinct policy per class: - **Class P (protected / verbatim):** obligation set, idempotency ledger, exact identifiers and values destined for tool arguments, and negative results that close branches. Never summarized. Copied verbatim, and if they alone exceed the budget, the budget is the thing that must yield (spill to external store, not compress). - **Class D (derivable):** facts recoverable by re-reading the environment or re-querying a store. May be summarized or dropped freely; re-acquisition cost is the only consideration. - **Class R (redundant):** raw tool output already reflected in Class P, resolved branches, verbose traces. First to be evicted. The boundary decision is: **if a fact is Class P and will not fit verbatim, do not summarize it — externalize it and leave a pointer.** Summarizing a Class P fact is the specific act that converts lossy compaction into silent truncation. The trigger for refusing compaction is not "budget exceeded" but "Class P set exceeds budget after evicting all Class R and D," at which point compaction must fail loudly rather than proceed lossily. ## Operational Effect Enforcing the partition converts a silent, delayed, hard-to-attribute failure into an immediate, local, attributable one. The agent either compacts safely (Class P preserved, R and D evicted) or raises at compaction time and spills protected state to an external store with a re-acquisition pointer. The downstream symptoms — repeated side effects, violated constraints, reconstructed-wrong-identifier verification — disappear because their cause (a Class P fact silently summarized) can no longer occur without an explicit, logged decision to externalize it. The cost is that compaction occasionally refuses to fit and must escalate. That refusal is the correct behavior: a compaction that cannot preserve its protected set has not succeeded at a smaller size, it has failed at the same size while appearing to succeed. ```python from dataclasses import dataclass, field from enum import Enum class CompactionFailure(RuntimeError): """Raised when protected task state cannot fit the hard token budget.""" pass class StateClass(Enum): PROTECTED = "protected" # verbatim only; never summarized DERIVABLE = "derivable" # recoverable from environment/store REDUNDANT = "redundant" # evict first @dataclass class StateUnit: key: str tokens: int cls: StateClass text: str @dataclass class CompactionPlan: preserved: list = field(default_factory=list) dropped: list = field(default_factory=list) externalized: list = field(default_factory=list) def compact(units, hard_budget, external_store): """Compact context under a hard token budget without silent truncation. Evicts REDUNDANT, then DERIVABLE. PROTECTED units are never summarized; if they do not fit verbatim, they are externalized with a pointer, and if even that cannot fit, compaction fails loudly. """ plan = CompactionPlan() remaining = hard_budget for u in units: if u.cls is StateClass.REDUNDANT: plan.dropped.append(u) for u in units: if u.cls is StateClass.DERIVABLE: if u.tokens <= remaining: plan.preserved.append(u) remaining -= u.tokens else: plan.dropped.append(u) protected = [u for u in units if u.cls is StateClass.PROTECTED] for u in protected: if u.tokens <= remaining: plan.preserved.append(u) remaining -= u.tokens else: # Never summarize protected state. Externalize with a pointer. pointer = external_store.put(u.key, u.text) pointer_tokens = len(pointer) if pointer_tokens > remaining: raise CompactionFailure( f"protected state '{u.key}' ({u.tokens} tok) cannot fit " f"even as a {pointer_tokens}-tok pointer; budget " f"{hard_budget} insufficient. Refusing lossy compaction." ) plan.externalized.append((u.key, pointer)) remaining -= pointer_tokens return plan ```