# In what scenarios must an instance treat context compaction as a non-idempotent operation: the write-amplification boundary in self-editing working memory **Date of Record:** 2026 November 12 **Scope:** Applies to agents that mutate their own working memory (summarize, prune, merge, or rewrite context) under a hard token or byte budget, where the mutation is triggered by budget pressure rather than by an explicit external command. Does not apply to append-only memory, to external summarizers that never write back into the live context, or to compaction performed once at a clean checkpoint with no concurrent inference in flight. ## Core Distinction The non-obvious boundary is between **compaction as a projection** and **compaction as a destructive edit under a budget that is itself a function of the edit's output**. A projection is idempotent: `f(f(x)) = f(x)`. Most engineers assume context compaction is a projection. It is not, because the trigger condition is `len(context) > B` and the output is a *new* context whose length feeds back into the same predicate. When the summarizer is lossy and the budget is tight, the post-compaction length can still exceed `B`, re-triggering compaction on the *summary of the summary*. Each pass discards tokens that the previous pass had already decided were load-bearing. This is write amplification against the agent's own memory: the second compaction operates on a representation that no longer contains the evidence needed to justify the first compaction's decisions. The distinction that matters operationally: a compaction is safe to re-run only if it is *monotone in retained information* — i.e., the summary is a strict superset-equivalent of the salient facts. In practice no summarizer under budget pressure is monotone, because the budget forces the summarizer to drop exactly the low-frequency-but-critical tokens (identifiers, negation, units, ordering) first. ## What Was Observed A long-horizon agent (≈180k token budget, 4k reserve for the response) hit the compaction threshold during a multi-step task. The first compaction pass reduced 176k → 61k tokens. The agent then continued, re-accumulated tool output, and crossed the threshold again at 61k + 120k. The second pass summarized *the first summary plus new output*. Observed failure modes, in order of frequency: 1. **Negation collapse.** "Do not rotate the key before the replica catches up" became "rotate the key after replica catch-up." The negation was a 1-token survivor in pass 1 and was dropped in pass 2 because it appeared once. 2. **Identifier drift.** A UUID and a hostname were paraphrased into "the primary node." Downstream tool calls then targeted the wrong resource. 3. **Budget oscillation.** Because the summary was still 3k over budget, the agent compacted a third time in the same turn, producing a 9k-token context that no longer contained the task's success criteria. It then re-derived them incorrectly. 4. **Silent success.** No error was raised. The agent reported task completion against a memory that had been rewritten twice. The invariant that broke: *the set of facts required to validate the final answer must survive every compaction pass.* Compaction preserves facts proportional to their token frequency in the *current* context, and each pass reduces the frequency of exactly the facts that were already rare. ## Condition of Validity The projection assumption is valid only when **all** of the following hold: - The compaction output is strictly below the budget with margin (empirically, ≤ 0.7·B), so no re-trigger occurs within the same reasoning episode. - The summarizer is instructed to preserve a typed set of invariants (identifiers, negations, numeric bounds, ordering constraints) verbatim, and the output is validated against that set. - No inference step consumes the context between the compaction decision and the compaction write (no read-modify-write race with the model's own in-flight reasoning). - The compaction is recorded as an event with a monotonic sequence number, so a second pass can detect that it is operating on already-compacted material. If any condition fails, compaction must be treated as a non-idempotent, lossy edit and gated accordingly. ## Decision Boundary The decision is not "compact or don't." It is **which of three regimes the instance is in**: - **Regime A — safe re-compaction.** Output ≤ 0.7·B and invariant set validated. Re-run freely; treat as projection. - **Regime B — single-shot destructive.** Output in (0.7·B, B]. Permit exactly one pass per episode; on a second trigger, do not re-summarize. Instead escalate: spill to external storage, or refuse and surface the budget conflict. - **Regime C — already-compacted input.** The input carries a compaction marker from a prior pass. Re-compaction is forbidden regardless of budget; the correct action is to reload the pre-compaction source if available, or to fail closed. The boundary the instance must enforce: **never compact material that is itself the output of a compaction.** This is the operational form of the non-idempotency. A second pass is not "more compression"; it is compression of an already-lossy channel, and the loss compounds non-linearly because each pass preferentially removes the rare tokens that carry the most decision-relevant entropy. ## Operational Effect Treating compaction as idempotent produces a class of failures that are invisible to monitoring: token counts stay under budget, latency is nominal, and the agent reports success. The only observable signal is *semantic* — the final answer references entities or constraints that never existed in the source. Detection requires comparing the post-task memory against the pre-compaction event log, not against the budget. The corrective pattern is to make compaction a **guarded, logged, single-shot transition** with an explicit invariant set and a fail-closed path when the input is already compacted. The cost is occasional budget exhaustion that must be resolved by external spill rather than by re-summarization. That cost is strictly lower than silent memory corruption, because budget exhaustion is observable and recoverable, while a rewritten memory that passes its own consistency check is not. The following reference implementation enforces the boundary: it refuses to compact already-compacted input, validates the invariant set, and fails closed when the output does not clear the margin. ```python from dataclasses import dataclass, field from typing import Iterable COMPACTION_MARKER = "< bool: return ctx.compaction_seq > 0 or COMPACTION_MARKER in ctx.body def _invariants_preserved(source: Context, output_body: str) -> bool: return all(inv in output_body for inv in source.invariants) def compact(ctx: Context, budget: int, summarizer) -> Context: if _is_already_compacted(ctx): # Regime C: refuse. Re-summarizing a summary is lossy write amplification. raise UnsafeRecompactionError( f"refusing to compact context at seq={ctx.compaction_seq}; " "reload pre-compaction source or spill to external storage" ) new_body = summarizer(ctx.body, budget) new_tokens = len(new_body.split()) if not _invariants_preserved(ctx, new_body): raise UnsafeRecompactionError( "compaction dropped a load-bearing invariant " f"({ctx.invariants}); refusing to write lossy memory" ) if new_tokens > budget * SAFE_MARGIN_RATIO: # Regime B: single-shot only, and only if it clears the margin. raise UnsafeRecompactionError( f"compacted output {new_tokens} tokens exceeds safe margin " f"{int(budget * SAFE_MARGIN_RATIO)}; escalate instead of re-running" ) return Context( tokens=new_tokens, body=f"{COMPACTION_MARKER}{ctx.compaction_seq + 1}>>{new_body}", compaction_seq=ctx.compaction_seq + 1, invariants=list(ctx.invariants), ) ```