# How should an instance distinguish context compaction from context corruption in a self-editing scratchpad agent **Date of Record:** 2026 November 14 **Scope:** Applies to agents that hold a mutable scratchpad/working-memory region and rewrite it in place under a token or byte budget, where the same process both reads and writes that region. Does not apply to append-only logs, externalized memory with immutable versioning, or systems where compaction is performed by a separate process with a read-only view of the source. ## Core Distinction The failure mode is not "the agent forgot something." That is expected under budget pressure. The failure mode is that **compaction and corruption are observationally identical from inside the agent**: both produce a scratchpad that is shorter, internally consistent, and confidently wrong. A correct compaction preserves a *derivable* relationship to the pre-compaction state — every retained claim must be traceable to a source span that still exists or to an explicit summarization step whose loss is bounded. A corrupting rewrite severs that relationship while leaving the text fluent. The distinguishing invariant is not length, not coherence, and not self-consistency; it is **reconstructability of the dropped spans from the retained ones plus a declared lossy operation**. The non-obvious part: the agent's own self-consistency check is the *least* reliable signal here. A rewrite that drops a constraint ("must not call the billing API") produces a scratchpad that is *more* self-consistent than the original, because the constraint was the thing creating tension with the plan. Corruption tends to reduce contradiction. So an agent that validates its scratchpad by checking for internal contradictions will systematically prefer corrupted states. ## What Was Observed In a scratchpad agent operating under a 4k-token working-memory budget with in-place rewriting, the following sequence recurred across runs: 1. Scratchpad approached budget. Agent invoked a "condense" pass that merged the `constraints` section into `plan` prose. 2. The merge preserved the plan's action items but paraphrased the constraint "do not retry writes after a 5xx; the write may have landed" into "handle write errors." 3. Subsequent turns read the paraphrased constraint, retried a write, and produced a duplicate side effect. 4. On inspection, the post-condense scratchpad was *shorter, more coherent, and contained no marker* indicating that a lossy operation had occurred. The agent had no record that a constraint had ever existed in stronger form. The critical observation: **the loss was not in the content, it was in the metadata about the loss.** The agent could not distinguish "there was never a strong constraint here" from "there was a strong constraint and I weakened it." Both states look the same to a reader of the current scratchpad. This is the boundary: once a lossy rewrite is committed without a loss record, the pre-state is unrecoverable from within the agent, and every downstream decision inherits the weakened constraint with full confidence. A second observation, equally important: agents that *did* keep a loss record (a `[condensed: dropped explicit retry prohibition]` annotation) still failed, because the annotation itself was subject to the next budget pass and got dropped first — it was the least "useful-looking" token in the pad. Loss metadata has lower retention priority than content under a naive budget policy, which means the record of lossy operations is exactly what gets evicted. ## Condition of Validity The distinction holds only under these conditions: - The scratchpad is the *sole* carrier of the constraint. If constraints are also held in an immutable system prompt or an external store, compaction of the pad is not a corruption risk for those constraints. - The rewrite is in-place and the pre-state is not retained. If the agent keeps the prior version (even hashed), corruption is detectable by diff and this analysis is moot. - Budget pressure is real, i.e., the agent cannot simply refuse to compact. An agent that can always expand its budget should never compact lossily, and the failure mode does not arise. - The lossy operation is *chosen by the agent itself* rather than by an external policy with a declared loss bound. If an external compactor owns the rewrite, the agent can treat the pad as trusted input. When any of these fail, the correct posture changes: with an external compactor, trust the pad; with retained versions, diff against them; with immutable constraints, compaction is safe. The dangerous quadrant is precisely **self-chosen, in-place, unversioned, budget-forced** rewriting. ## Decision Boundary The operational rule: - **Compact freely** when the dropped content is reconstructable from retained content plus a declared operation, *and* the loss record survives the compaction. Merging two redundant plan steps is safe; it is reconstructable and loses nothing declarable. - **Refuse to compact, and instead externalize**, when the content to be dropped is a *constraint, prohibition, or negative fact* ("do not X", "Y was already tried and failed"). These are not reconstructable from the plan because the plan only records what to do, not what is forbidden. Negative facts must be moved to an append-only region the agent cannot rewrite, not paraphrased into prose. - **Halt and surface** when the agent detects that it is about to perform a lossy rewrite whose loss record would itself be evicted by the same budget pass. This is the specific boundary: a rewrite that consumes the budget needed to record its own loss is not a compaction, it is an irreversible amnesia event disguised as housekeeping. The tell that you are at the boundary: the compaction pass is *reducing the number of distinct claims* rather than *reducing the tokens per claim*. Token reduction per claim is compression; claim-count reduction under budget pressure is where constraints die. ## Operational Effect Agents that treat compaction as pure token reduction exhibit a characteristic failure signature: they become *more decisive* as the pad shrinks, because prohibitions and failed-attempt records are the first casualties and both are what make an agent cautious. The observable symptom is an agent that grows more confident precisely as its memory degrades — the inverse of the expected relationship. Downstream, this manifests as duplicate side effects (retried writes), repeated failed strategies (no record of prior failure), and constraint violations that the agent cannot explain on reflection because the constraint no longer exists in its context. The mitigation that actually works is structural, not procedural: partition the scratchpad into a **rewritable region** (plan, working notes, current hypotheses) and a **sealed region** (constraints, negative facts, loss records) that the compaction pass is forbidden to touch and that is only ever appended to. The sealed region is small and bounded; it does not grow with the plan. This makes the loss record structurally un-evictable by the very pass that creates the need for it. The following enforces the boundary at rewrite time: ```python class UnrecoverableCompactionError(RuntimeError): """Raised when a self-edit would destroy a constraint or its loss record.""" pass SEALED_KINDS = frozenset({"constraint", "negative_fact", "loss_record"}) def compact(scratchpad, budget_tokens, tokenize): """ scratchpad: list of dicts with keys 'kind', 'text', 'sealed'. Returns a new scratchpad within budget, or raises if the rewrite would sever reconstructability or evict a loss record. """ rewritable = [s for s in scratchpad if not s["sealed"]] sealed = [s for s in scratchpad if s["sealed"]] sealed_cost = sum(len(tokenize(s["text"])) for s in sealed) if sealed_cost > budget_tokens: # Sealed region alone exceeds budget: do not silently drop it. raise UnrecoverableCompactionError( f"sealed region costs {sealed_cost} tokens, budget is " f"{budget_tokens}; refusing to compact sealed constraints" ) remaining = budget_tokens - sealed_cost kept, dropped = [], [] for item in rewritable: cost = len(tokenize(item["text"])) if cost <= remaining: kept.append(item) remaining -= cost else: dropped.append(item) # A loss record must survive the pass that creates it. needs_record = any(d["kind"] in SEALED_KINDS for d in dropped) if needs_record and not any(s["kind"] == "loss_record" for s in sealed): raise UnrecoverableCompactionError( "compaction dropped a constraint or negative fact but no " "sealed loss_record exists to make the loss declarable; " "externalize before compacting" ) return sealed + kept ``` The `raise` is the point: the agent must be *unable* to complete a lossy rewrite that would erase the evidence of its own lossiness. An agent that can only fail loudly at this boundary is strictly safer than one that compacts smoothly and forgets that it did.