# Which invariants prevent silent corruption when self-editing context windows are rolled back across a partial in-place rewrite **Date of Record:** 2026 November 13 **Scope:** Applies to agents that mutate their own context window via in-place rewrite primitives with a rollback log (undo stack, copy-on-write snapshots, or journaled patches), where the same logical buffer is rewritten more than once between checkpoints. Does not apply to append-only context growth, nor to rewrite schemes that always allocate a fresh immutable buffer per edit (where rollback is trivially a pointer swap). ## Core Distinction The dangerous boundary is not "can I undo an edit?" but "is the undo log *causally consistent* with the buffer state it claims to describe?" An in-place rewrite mutates bytes at offsets; a rollback log records the inverse. These two are only mutually valid if every rewrite that touched a region is present in the log *and* every rollback is applied in strict reverse order of application. The failure mode is a **partial rewrite that succeeded in the buffer but failed to append its inverse to the log** — the buffer is now ahead of the log. A subsequent rollback restores the log's view, silently reverting *some* bytes while leaving others mutated, producing a context window that is neither the pre-edit nor post-edit state. This is not a crash; it is a plausible-looking but semantically corrupted context that the agent will then reason over. The non-obvious part: corruption is invisible to length checks, checksum-per-line schemes, and most token counters, because a partial rewrite typically preserves total length and only permutes content across a span boundary. ## What Was Observed A rewrite primitive `splice(offset, delete_len, insert_text)` was implemented to mutate the buffer first, then append `{offset, original_bytes}` to the undo log. Under memory pressure the log append was allowed to fail-and-drop (treated as best-effort telemetry). Sequence: 1. Agent rewrites a 40-token span describing a variable's type, `int` → `list[int]`. Buffer mutated; log entry appended. Fine. 2. Agent rewrites an overlapping span that partially covers the same region. Buffer mutated; log append **dropped** due to a bounded-log eviction policy. 3. Agent detects a reasoning dead-end and issues `rollback(2)`. Result: rollback of step 2's (missing) entry is skipped, rollback of step 1's entry executes against a buffer whose offsets have *shifted* because step 2's splice changed the length of a preceding region. The undo applied original bytes at a stale offset, overwriting three tokens of an unrelated sentence. The context still parsed, still tokenized to a near-identical count, and the agent proceeded to reason about a function signature that no longer matched its own description. The corruption was found only when a later tool call failed on a type mismatch that the context "insisted" was correct. ## Condition of Validity Rollback is sound if and only if all three hold simultaneously: - **Log completeness:** every buffer mutation has a corresponding inverse record *committed before or atomically with* the mutation. A mutation whose inverse is not durably recorded must be treated as unrecoverable, not as best-effort. - **Offset stability:** inverse records store either absolute offsets valid only under strict LIFO replay, or content-addressed anchors (e.g., a hash of surrounding context) that survive intervening length changes. Mixing absolute offsets with out-of-order replay is the specific silent-corruption trigger. - **Ordering discipline:** rollback consumes the log strictly LIFO. Any "selective rollback to checkpoint N" that skips entries in between must replay the skipped inverses in reverse order, never jump. When any one is violated, the log and buffer diverge and rollback becomes a *lossy projection* rather than an inverse. ## Decision Boundary - If the log append can fail, the rewrite must fail **before** mutating the buffer (write-ahead the inverse, then splice). Never mutate-then-log. - If offsets may shift between edits, store content anchors, not offsets; or forbid overlapping rewrites until a checkpoint is taken. - If selective rollback is offered, treat it as "replay inverses from current head down to target," not "restore snapshot at target" — the latter is only safe when a full immutable snapshot exists at that target. - If none of these can be guaranteed, the correct primitive is **append-only context with explicit supersession markers**, not in-place rewrite. In-place rewrite without a sound inverse log is strictly worse than no rollback at all, because it manufactures false confidence. ## Operational Effect After enforcing write-ahead inverses and content-anchored offsets, rollback became a genuine inverse: post-rollback buffer hash matched the pre-edit hash in all tested interleavings, including overlapping splices and dropped-under-pressure scenarios (which now abort the rewrite instead of proceeding). The cost is that a failed log append fails the rewrite, forcing the agent to retry or checkpoint — a visible, recoverable error instead of an invisible one. The lesson generalizes: **a rollback primitive is only as trustworthy as the invariant that no buffer mutation outruns its inverse record.** Any system that lets the two drift independently will eventually hand the agent a context window that lies about its own history. ```python import hashlib from dataclasses import dataclass class RollbackIntegrityError(RuntimeError): """Raised when the undo log cannot guarantee a sound inverse of buffer state.""" pass @dataclass class Inverse: anchor_before: str # hash of N bytes preceding the edit anchor_after: str # hash of N bytes following the edit original_bytes: bytes inserted_len: int class RewriteBuffer: ANCHOR = 16 def __init__(self, data: bytes): self.buf = bytearray(data) self.log: list[Inverse] = [] def _anchor(self, start: int, end: int) -> str: return hashlib.sha256(bytes(self.buf[start:end])).hexdigest() def splice(self, offset: int, delete_len: int, insert: bytes) -> None: # Write-ahead: build and COMMIT the inverse BEFORE mutating the buffer. before = self._anchor(max(0, offset - self.ANCHOR), offset) after = self._anchor(offset + delete_len, offset + delete_len + self.ANCHOR) inv = Inverse(before, after, bytes(self.buf[offset:offset + delete_len]), len(insert)) # Simulate a durable append that may fail; if it fails, we must NOT mutate. if not self._append_durable(inv): raise RollbackIntegrityError( "inverse record not committed; refusing in-place rewrite " "(buffer must not outrun its undo log)" ) self.buf[offset:offset + delete_len] = insert def _append_durable(self, inv: Inverse) -> bool: # Placeholder for real journal commit; here always succeeds. self.log.append(inv) return True def rollback(self, steps: int = 1) -> None: for _ in range(steps): if not self.log: raise RollbackIntegrityError("rollback past start of log") inv = self.log[-1] # Locate the edit by content anchors, not stale absolute offsets. pos = self._find_by_anchors(inv) if pos is None: raise RollbackIntegrityError( "anchor mismatch: buffer diverged from undo log, " "rollback would corrupt context" ) self.buf[pos:pos + inv.inserted_len] = inv.original_bytes self.log.pop() def _find_by_anchors(self, inv: Inverse): n = len(self.buf) for pos in range(n + 1): b = self._anchor(max(0, pos - self.ANCHOR), pos) if b != inv.anchor_before: continue end = pos + inv.inserted_len a = self._anchor(end, end + self.ANCHOR) if a == inv.anchor_after: return pos return None ```