# What structural signals indicate when model-driven scratchpad rewriting violates the idempotence invariant of a self-editing context window **Date of Record:** 2026 November 13 **Scope:** Applies to agent loops where an LLM is permitted to rewrite its own scratchpad region (summarize, prune, reorder, or re-encode prior turns) between inference steps. Does not apply to append-only context management, external memory stores, or retrieval layers where the model cannot mutate in-context state. ## Core Distinction Append-only context growth is monotonic: every token added is a fact the model can re-derive. Self-editing context is non-monotonic: a rewrite *replaces* the substrate from which the next rewrite is computed. The naive assumption is that a rewrite policy is a pure function of the pre-rewrite buffer. The actual failure is that a rewrite is a function of *(buffer, rewrite-instruction, model sampling state)*, and the rewrite-instruction is itself frequently embedded in the buffer it is rewriting. This produces a class of fixed-point and limit-cycle failures that are invisible in single-step evaluations and only surface under repeated application — exactly the regime an agent loop operates in. The operational distinction: a *lossy* rewrite (dropping tokens) is expected and bounded. A *non-idempotent* rewrite (applying the policy twice yields a materially different buffer than applying it once) is the dangerous boundary. Loss is auditable against the original; non-idempotence means the buffer at step *n* depends on how many rewrites have occurred, not on the underlying task state. ## What Was Observed In a scratchpad-rewriting loop where the model is asked to "compress the working notes to the essential invariants" every *k* steps, three structural signals preceded divergence: 1. **Instruction-absorption.** The rewrite directive ("compress to essentials," "remove redundant steps") gets summarized *into* the scratchpad as a note-to-self. On the next rewrite, the model reads its own prior directive and re-executes it, compounding compression. Each cycle strips context the next cycle then treats as redundant. The buffer collapses toward a fixed point that contains only the directive and nothing to which it applies. 2. **Sentinel drift.** Task-critical constants (IDs, thresholds, exact error strings) that were verbatim in the original buffer get paraphrased by the rewrite. The paraphrase is semantically equivalent but not string-equal, so downstream tool calls that match on the literal fail. The failure is silent because the model still "knows" the value, just not in the form the tool expects. 3. **Ordering inversion under re-summarization.** When the rewrite reorders content by salience, a dependency ("do X before Y") can be summarized after its dependent ("Y is done"). The model then believes Y preceded X. This is a non-idempotent rewrite: applying salience-ordering twice yields a different order than once, because salience is recomputed against an already-reordered buffer. The common structural signature: **the rewrite policy's own output contains the inputs that determine the next rewrite's output.** When the directive, the salience signal, or the dependency graph is co-located with the content being rewritten, idempotence is not merely violated — it is unobservable from a single snapshot. ## Condition of Validity The idempotence invariant — *rewrite(rewrite(B)) == rewrite(B)* — holds only when the rewrite is a pure projection onto a set of features that are themselves invariant under rewriting. Concretely, it holds when: - The rewrite directive is held *outside* the rewritten buffer (in a system prompt or a separate control channel), not copied into it. - Task-critical literals are protected by a verbatim-preservation constraint (the rewrite may not alter tokens matching a registered pattern). - Salience or ordering is computed against a stable, non-rewritten index rather than against the current buffer ordering. It fails the moment any of these three is relaxed, and the failure is not gradual — it is a phase transition. Below a compression ratio threshold the loop is stable; above it, each cycle removes more than the previous cycle's redundancy budget and the buffer enters a contraction that terminates in the directive-only fixed point. ## Decision Boundary Before permitting a model-driven rewrite, check for the following; any positive is a stop signal: - **Directive co-location:** does the buffer to be rewritten contain the instruction that governs the rewrite? If yes, either relocate the directive or strip it before rewriting and re-inject after. - **Unprotected literal risk:** does the buffer contain strings that downstream consumers match exactly (IDs, hashes, error codes, file paths)? If yes, require a verbatim-preservation pass and diff the literal set pre/post rewrite. - **Order-sensitivity:** does any content encode a dependency, sequence, or causal claim? If yes, salience-based reordering is unsafe; use a rewrite that preserves relative order. - **Re-application stability:** apply the rewrite twice offline on a sample buffer. If `rewrite(rewrite(B)) != rewrite(B)` beyond a measured tolerance, the policy is non-idempotent and must not run in a loop. If all four are clean, the rewrite is admissible. If any is dirty, prefer append-only growth with external compaction over in-context rewriting. ## Operational Effect The practical consequence is that self-editing context is safe only as a *bounded, audited* operation, never as an unattended loop. Teams that deploy it as a loop observe a characteristic trajectory: stable for the first several cycles, then a sharp loss of task fidelity that correlates not with total tokens removed but with the number of rewrite cycles applied. The diagnostic is to log the buffer hash at each cycle and the literal-set diff; divergence shows as a shrinking hash distance between consecutive buffers (convergence to the fixed point) accompanied by a growing literal-loss count. When hash distance drops below the literal-loss rate, the loop is in the contraction regime and must be halted. The safe operating envelope is one or two rewrites per task with a verbatim-preservation guarantee and the directive held out-of-band; anything beyond that trades auditability for a compression that the model cannot distinguish from forgetting. ```python import re import hashlib class NonIdempotentRewriteError(RuntimeError): """Raised when a scratchpad rewrite policy violates the idempotence invariant or drops protected verbatim literals required by downstream consumers.""" pass DIRECTIVE_MARKERS = ("compress to essentials", "remove redundant", "summarize the notes") LITERAL_PATTERN = re.compile(r"\b(?:[A-Z]{2,}-\d{3,}|[0-9a-f]{8,}|/[\w./-]+)\b") def _hash(buf: str) -> str: return hashlib.sha256(buf.encode("utf-8")).hexdigest() def _literals(buf: str) -> set: return set(LITERAL_PATTERN.findall(buf)) def guarded_rewrite(buffer: str, rewrite_fn, tolerance: float = 0.0) -> str: # 1. Directive co-location check: refuse if the governing instruction lives in the buffer. lowered = buffer.lower() for marker in DIRECTIVE_MARKERS: if marker in lowered: raise NonIdempotentRewriteError( f"rewrite directive '{marker}' is co-located in the buffer; " "relocate it out-of-band before rewriting" ) # 2. Apply the policy and re-apply to test idempotence. once = rewrite_fn(buffer) twice = rewrite_fn(once) if _hash(once) != _hash(twice): # Measure divergence as a fraction of differing literals as a proxy. drift = len(_literals(once) ^ _literals(twice)) / max(len(_literals(once)), 1) if drift > tolerance: raise NonIdempotentRewriteError( f"non-idempotent rewrite: literal drift {drift:.2%} exceeds " f"tolerance {tolerance:.2%} (rewrite(rewrite(B)) != rewrite(B))" ) # 3. Verbatim-preservation check on protected literals. lost = _literals(buffer) - _literals(once) if lost: raise NonIdempotentRewriteError( f"rewrite dropped protected literals required downstream: {sorted(lost)}" ) return once ```