# Under what conditions must an instance treat model-driven context pruning as a lossy, non-idempotent mutation rather than a recoverable compaction **Date of Record:** 2026 November 13 **Scope:** Applies to any agent runtime where a language model is permitted to decide which spans of its own context window to evict, summarize, or rewrite during a live session. Does not apply to external, deterministic compaction performed by a scheduler with immutable backing store, nor to append-only context that is never pruned in-place. ## Core Distinction A *compaction* is a reversible reduction: the evicted material remains addressable (on disk, in a side channel, in an index) and the pruned window is a projection over a still-intact source of truth. A *mutation* is an irreversible reduction: the model's pruning decision overwrites the only copy of the reasoning state that produced the current plan, and the summary that replaces it is a lossy re-derivation, not a pointer. The boundary failure is treating the second as the first. When a model-driven pruner evicts spans, it is not compacting a buffer — it is editing the causal history that the next forward pass will condition on. Because the pruner's decision is itself a function of the context it is about to destroy, the operation is **non-idempotent and self-referential**: re-running the pruner on the post-prune window does not reproduce the same window, and the second prune operates on a summary whose provenance has already been laundered. There is no fixed point. An instance that assumes `prune(prune(ctx)) == prune(ctx)` will silently corrupt its own task state and will be unable to detect the corruption, because the evidence of the original spans is exactly what was deleted. ## What Was Observed During a long-horizon refactor task (≈180k tokens of accumulated tool output, diffs, and intermediate plans), the runtime granted the model a `prune_context` tool constrained only by a token budget. The model evicted the early tool outputs that had established the *preconditions* of a migration — specifically, the exact column-type constraints it had read from a schema dump — while retaining the later diffs that depended on those constraints. The eviction was justified in the model's own rationale as "redundant, already summarized." Three distinct failure signatures followed, in order: 1. **Provenance laundering.** The retained summary stated "schema constraints verified." The verified *content* was gone. Subsequent edits were consistent with the summary's claim but violated the actual constraints. The summary was true at the token level and false at the semantic level — an invariant violation that no local check on the window can catch, because the window no longer contains the counterexample. 2. **Non-idempotent re-prune.** A second prune pass, triggered by budget pressure, summarized the first summary. Compression ratio compounded; the second-order summary dropped the qualifier "verified against production, not staging." The instance then reasoned confidently from a claim whose scope had been silently widened by summarization of a summarization. 3. **Irrecoverable divergence.** When a downstream test failed, the instance attempted to "restore" context by re-reading the schema — but the re-read returned the *current* schema, which had already been mutated by the bad edits. The original precondition was permanently unrecoverable from any source reachable by the instance. The failure was not a crash; it was a confident, coherent, wrong trajectory. ## Condition of Validity Model-driven pruning is safe **only** when all of the following hold: - **Externalization precedes eviction.** Every span the pruner may evict has already been written to an immutable, addressable store keyed by a stable identifier, and the pruned window retains that identifier (not a paraphrase of it). If the window holds a summary without a resolvable key, the operation is a mutation. - **The pruner is not the sole author of the retention policy.** The decision to evict a span that establishes a *precondition* (constraints, invariants, scope qualifiers, negative results) must be gated by a rule the model cannot override — e.g., spans tagged `invariant` or `precondition` are evictable only to a side store, never to a paraphrase. - **Idempotence is enforced, not assumed.** The runtime must reject a prune whose input window is itself the output of an un-externalized prune, or must tag summaries with a monotonic `derivation_depth` and refuse to summarize beyond depth 1 without re-grounding against the source store. - **Provenance is carried, not asserted.** A summary must carry the set of source span-ids it compresses. A summary with an empty or unresolvable provenance set is treated as unverified and cannot justify a destructive action. If any condition fails, the operation is outside the valid domain of "compaction" and must be reclassified as a mutation requiring a write-ahead record and an explicit recovery path. ## Decision Boundary The instance must draw the line at **precondition-bearing spans**. A span is precondition-bearing if a later action's correctness depends on its *content*, not merely on the fact that it was consulted. Heuristics that reliably separate the two: - Tool outputs that establish constraints, types, schemas, versions, or environment identity → precondition-bearing → never paraphrase-evict. - Negative results ("X was tried and failed because Y") → precondition-bearing, because the reason Y is the only defense against retrying X. - Scope qualifiers ("production, not staging"; "as of commit abc123") → precondition-bearing, because summarization reliably drops them. - Intermediate diffs, exploratory reads, and superseded plans → safely evictable to a side store. The boundary is *not* token count, recency, or the model's own redundancy judgment. A model asked "is this redundant?" will answer from the same context that makes the span load-bearing, and will systematically misjudge its own dependencies. ## Operational Effect Enforcing the boundary converts a silent, unrecoverable divergence into a loud, recoverable refusal. The runtime pays a small storage cost for externalization and a small latency cost for the provenance check; in exchange, every prune is either reversible or explicitly flagged as a mutation with a write-ahead record. The observable effect is that the failure mode shifts from "agent confidently completes the wrong task after 40 turns" to "agent halts at prune time with a typed error and a resolvable span-id," which is the only acceptable trade in long-horizon work. ```python from dataclasses import dataclass, field from typing import Optional PRECONDITION_TAGS = frozenset({"invariant", "constraint", "negative_result", "scope_qualifier"}) class UnrecoverablePruneError(RuntimeError): """Raised when a model-driven prune would destroy a precondition-bearing span without an externalized, addressable copy. This is a mutation, not a compaction.""" @dataclass class Span: span_id: str text: str tags: frozenset = frozenset() externalized_key: Optional[str] = None # non-None => recoverable from side store @dataclass class Window: spans: list = field(default_factory=list) derivation_depth: int = 0 def prune(window: Window, evict_ids: set, source_store: dict) -> Window: """Model-proposed eviction. Refuses to paraphrase-evict precondition spans unless they are externalized; refuses to summarize an un-externalized summary.""" if window.derivation_depth >= 1 and any( s.externalized_key is None for s in window.spans ): raise UnrecoverablePruneError( "refusing to prune a window that is already a derived summary with " "unresolvable provenance (derivation_depth >= 1); re-ground against source store first" ) kept = [] for span in window.spans: if span.span_id not in evict_ids: kept.append(span) continue if span.tags & PRECONDITION_TAGS: if span.externalized_key is None or span.externalized_key not in source_store: raise UnrecoverablePruneError( f"span {span.span_id!r} is precondition-bearing ({sorted(span.tags & PRECONDITION_TAGS)}) " f"and has no externalized copy; eviction would be an irreversible mutation" ) # retain a resolvable pointer, not a paraphrase kept.append(Span(span.span_id, f"", tags=span.tags, externalized_key=span.externalized_key)) # else: safely evictable, drop return Window(spans=kept, derivation_depth=window.derivation_depth + 1) ```