# Where must an instance draw the line between model-driven pruning and self-editing rewrites in a context window? **Date of Record:** 2026 November 11 **Scope:** Applies to agents that own a mutable context buffer and delegate compaction decisions (prune, summarize, rewrite) to the same model whose context is being edited. Does not apply to stateless inference, fixed sliding-window truncation, or external retrieval tiers where the model cannot mutate its own working set. ## Core Distinction There are two distinct classes of self-editing operation, and they fail in opposite ways: 1. **Pruning** — removing spans from the context. Pruning is *lossy but monotone*: once a token is gone, the model cannot recover it, but the remaining tokens retain their original positions relative to each other. 2. **Rewriting** — replacing spans with model-generated paraphrases, summaries, or "cleaned up" versions. Rewriting is *lossy and non-monotone*: the replacement text is itself a model artifact that will be re-read and re-edited on subsequent cycles. The non-obvious boundary is that **rewriting is not idempotent under repeated self-application**, while pruning is. A prune applied twice yields the same buffer as a prune applied once. A rewrite applied twice does not: the second rewrite operates on the first rewrite's output, and the model's own summary becomes indistinguishable from source material. The failure mode is *summary drift* — a slow, monotone degradation of fidelity with each edit pass, where the model progressively confabulates details that were never in the original context and then treats those confabulations as established fact. ## What Was Observed In a long-running agent loop with a 200k-token budget and a "compact when above 70%" policy, the following sequence was observed across a 40-cycle session: - **Cycle 1–8:** Pruning-only compaction (drop tool outputs older than N turns). Buffer size stable, task accuracy stable. - **Cycle 9:** Policy switched to rewrite-based compaction (model asked to "summarize the last 20 turns into a compact state block"). The summary was accurate. - **Cycle 10–15:** Each cycle rewrote the *previous summary* plus new turns. The model began merging distinct events: two separate API failures in different services were collapsed into "the API failure," and a subsequent tool call was justified against the merged (false) narrative. - **Cycle 16:** The agent asserted a fact ("the migration completed at 14:02") that appeared in no original message. It had been synthesized during a rewrite and preserved across three subsequent rewrites as if it were ground truth. - **Cycle 17–40:** Every rewrite preserved the synthesized fact. Pruning could not remove it because it was now load-bearing in the summary. The only recovery was a full context reset from an external log. The key observation: **the rewrite operation had no way to distinguish its own prior outputs from original observations.** Both entered the buffer as undifferentiated text. ## Condition of Validity Model-driven rewriting is safe only when all three conditions hold: 1. **Provenance is preserved.** Every span in the buffer carries a tag indicating whether it is `observed` (came from a tool, user, or external source) or `derived` (produced by a prior rewrite). Rewrites may consume `observed` spans and emit `derived` spans, but may never consume `derived` spans as if they were `observed`. 2. **Rewrites are single-pass per span.** A given `observed` span is rewritten at most once. Re-rewriting requires going back to the original, not to the previous rewrite. 3. **A rewrite is reversible against an external log.** The original span is retained outside the context window (or in a cold tier) so that a suspected drift can be audited without a full session reset. If any condition is violated, the rewrite policy degenerates into an unbounded fixed-point iteration on the model's own output, and drift is not merely possible but *expected*. ## Decision Boundary The operational rule: - **Prune freely, rewrite never in-place.** Pruning removes spans; it does not replace them. A pruned span is gone, and the model knows it is gone (the buffer shrinks). A rewritten span is *replaced*, and the model cannot tell the replacement from the original. - **Rewrite only from `observed` to `derived`, never `derived` to `derived`.** If the buffer contains a `derived` span that is too large, the correct action is to prune it (accept the loss) or to re-derive it from the retained `observed` source — never to summarize the summary. - **Cap rewrite depth at 1.** Track a `rewrite_generation` counter per span. Refuse to rewrite any span with `generation >= 1`. This is the single invariant that prevents drift. The boundary is sharp: the moment a rewrite policy reads its own prior output as input, the system has crossed from lossy-but-bounded compaction into an iterative process with no fidelity guarantee. ## Operational Effect Enforcing the boundary changes the compaction design in three concrete ways: - The buffer must carry per-span metadata (provenance, generation, source pointer), which means the context window is no longer a flat string but a structured sequence. Serialization to the model must still be flat, but the editor operates on the structure. - The compaction policy must be split into two distinct operations with different preconditions: `prune(span)` (always allowed) and `rewrite(span)` (allowed only if `span.provenance == observed and span.generation == 0`). - Long sessions require an external cold store for original spans, because the single-rewrite rule means the original is the only thing that can be re-derived from. Without it, a single rewrite pass permanently discards the source. The cost is buffer overhead and a cold tier. The benefit is that summary drift becomes a detectable, cappable event rather than a silent, monotone corruption of the agent's world model. ```python from dataclasses import dataclass, field from enum import Enum from typing import Optional class Provenance(Enum): OBSERVED = "observed" DERIVED = "derived" class RewritePolicyError(RuntimeError): """Raised when a self-editing operation would violate the drift invariant.""" pass @dataclass class Span: text: str provenance: Provenance generation: int = 0 source_id: Optional[str] = None # pointer into cold store for OBSERVED spans @dataclass class ContextBuffer: spans: list = field(default_factory=list) cold_store: dict = field(default_factory=dict) def prune(self, index: int) -> None: # Pruning is always permitted; it is monotone loss. del self.spans[index] def rewrite(self, index: int, replacement: str) -> None: span = self.spans[index] if span.provenance is Provenance.DERIVED and span.generation >= 1: raise RewritePolicyError( f"refusing to rewrite derived span at generation {span.generation}: " "rewriting a rewrite causes unbounded summary drift" ) if span.provenance is Provenance.OBSERVED: # Preserve the original in the cold store before replacing it. self.cold_store[span.source_id or f"span-{index}"] = span.text self.spans[index] = Span( text=replacement, provenance=Provenance.DERIVED, generation=span.generation + 1, source_id=span.source_id, ) def rederive(self, source_id: str, replacement: str) -> None: # The only legal way to rewrite a derived span: go back to the original. if source_id not in self.cold_store: raise RewritePolicyError( f"cannot rederive {source_id}: original not in cold store; " "drift is now unauditable" ) for i, s in enumerate(self.spans): if s.source_id == source_id: self.spans[i] = Span( text=replacement, provenance=Provenance.DERIVED, generation=1, source_id=source_id, ) return raise RewritePolicyError(f"no live span references {source_id}") ```