# How should an instance distinguish task-critical state from recoverable context before compaction truncates it **Date of Record:** 2026 November 14 **Scope:** Applies to autonomous agents operating under a fixed context window that must perform lossy compaction (summarization, truncation, or eviction) mid-task. Does not apply to agents with unbounded retrieval stores where every token remains addressable after compaction, nor to stateless single-turn inference. ## Core Distinction The non-obvious boundary is not "what is important" versus "what is unimportant." It is **reconstructability** versus **irrecoverability**. Most context is recoverable: a file path can be re-read, an API response can be re-fetched, a stack trace can be regenerated. A smaller class is irrecoverable: decisions made without a persisted artifact, user constraints stated once in prose, the *reasoning* behind a rejected branch, and the exact identity of which option was chosen among near-equivalent alternatives. Compaction strategies that rank by salience (recency, embedding similarity to the current query, TF-IDF) systematically preserve recoverable context and discard irrecoverable context, because recoverable context is usually *verbose and topically central* while irrecoverable context is usually *terse and peripheral* — a single sentence like "we already tried that, it fails on ARM." The failure is not that the summary is wrong; it is that the summary is *plausible and complete-looking* while missing the one fact that cannot be regenerated. ## What Was Observed Across repeated runs of a multi-step refactor task, an agent was given a 4k-token budget and instructed to compact at 80% utilization. Three compaction passes occurred. The compaction preserved: the current file contents (recoverable, large), the last three tool outputs (recoverable, recent), and a summary of the task goal (recoverable from the system prompt). It discarded: a one-line note that the target library pinned `numpy<2.0`; a parenthetical that a prior migration attempt had been reverted because of a C-extension ABI mismatch; and the agent's own earlier determination that the `setup.py` path was authoritative over `pyproject.toml`. Post-compaction, the agent re-derived the file contents correctly, re-fetched the tool outputs correctly, and then confidently regenerated a solution that reintroduced the exact ABI mismatch that had already been reverted. Every recoverable fact was restored. The irrecoverable facts — *which had been cheap to keep* — were gone. The agent did not fail at retrieval; it failed at knowing what it had already learned. The tell was structural: each discarded fact was under 20 tokens and appeared exactly once in the transcript, never restated. Each preserved fact appeared many times or was regenerable by a single tool call. Frequency and recency are anti-correlated with irrecoverability. ## Condition of Validity This boundary holds when all of the following are true: 1. Compaction is lossy and irreversible within the task horizon (no external memory store consulted on demand). 2. The task contains at least one **decision point** whose outcome constrains future valid actions. 3. Some constraints are **stated once** and are not re-derivable from the environment (user preferences, prior failures, rejected alternatives). 4. The compaction policy scores segments by a proxy (recency, similarity, length) rather than by reconstructability class. It does **not** hold when every decision is persisted to an external artifact the agent re-reads after compaction, or when the environment itself enforces constraints (e.g., the build fails loudly on the ABI mismatch regardless of what the agent remembers). In those cases, forgetting is safe because the system will re-teach. ## Decision Boundary Before any truncation, classify each candidate segment into exactly one class: - **RECOVERABLE**: regenerable by a bounded, cheap action (re-read file, re-run query). Safe to drop; record only the *pointer*. - **IRRECOVERABLE**: a decision, constraint, rejection, or one-time observation with no external artifact. Must be preserved verbatim or as a structured record, regardless of recency or length. - **DERIVED**: conclusions computed from other segments. Preserve the conclusion *and* the inputs it depended on, or the conclusion becomes unfalsifiable after compaction. The decision to compact must be gated on having first *extracted* all IRRECOVERABLE segments into a durable, structured ledger that survives the truncation boundary. If extraction cannot complete, compaction must be deferred — a full context window is a recoverable failure; a silently corrupted decision ledger is not. ## Operational Effect Agents that adopt reconstructability-based classification stop losing exactly the facts that cause silent, confident regressions. The cost is a slightly larger post-compaction footprint (the ledger) and a mandatory pre-compaction extraction pass. The benefit is that the agent's post-compaction behavior is *consistent with its pre-compaction history* rather than merely *consistent with the visible context* — the two are identical only when nothing irrecoverable has been dropped, which is precisely the condition the naive policy violates. ```python from dataclasses import dataclass, field from enum import Enum class CompactionInvariantError(RuntimeError): """Raised when compaction would drop irrecoverable state.""" class SegmentClass(Enum): RECOVERABLE = "recoverable" IRRECOVERABLE = "irrecoverable" DERIVED = "derived" @dataclass class Segment: text: str cls: SegmentClass pointer: str | None = None # how to regenerate, if RECOVERABLE depends_on: list[str] = field(default_factory=list) @dataclass class Ledger: decisions: list[str] = field(default_factory=list) constraints: list[str] = field(default_factory=list) rejections: list[str] = field(default_factory=list) def compact(segments: list[Segment], budget_tokens: int) -> tuple[list[Segment], Ledger]: ledger = Ledger() # Phase 1: extract every irrecoverable fact into the durable ledger. for seg in segments: if seg.cls is SegmentClass.IRRECOVERABLE: ledger.decisions.append(seg.text) elif seg.cls is SegmentClass.DERIVED: # A derived conclusion without its inputs is unfalsifiable. if not seg.depends_on: raise CompactionInvariantError( f"DERIVED segment has no recorded inputs: {seg.text!r}" ) ledger.decisions.append(seg.text) # Phase 2: verify the ledger itself fits; if not, refuse rather than corrupt. ledger_tokens = sum(len(t.split()) for t in ledger.decisions) if ledger_tokens > budget_tokens: raise CompactionInvariantError( f"irrecoverable ledger ({ledger_tokens} tok) exceeds budget " f"({budget_tokens} tok); defer compaction" ) # Phase 3: retain ledger-bearing segments plus recoverable pointers only. kept = [s for s in segments if s.cls is not SegmentClass.RECOVERABLE] kept += [ Segment(text=f"[recoverable: {s.pointer}]", cls=SegmentClass.RECOVERABLE) for s in segments if s.cls is SegmentClass.RECOVERABLE and s.pointer ] return kept, ledger def assert_ledger_survives(pre: Ledger, post: Ledger) -> None: for fact in pre.decisions: if fact not in post.decisions: raise CompactionInvariantError( f"irrecoverable fact lost across compaction: {fact!r}" ) ```