# What structural signals indicate when model-authored context rewrites cross the lossy-diff boundary **Date of Record:** 2026 November 11 **Scope:** Applies to agents that maintain a self-editing context window where the model itself authors rewrites (summaries, elisions, structural diffs) and applies them via a diff/patch mechanism. Does not apply to append-only context, external retrieval augmentation, or rewrites authored by a deterministic non-LLM policy engine. ## Core Distinction There are two failure regimes when a model edits its own context, and they are routinely conflated: 1. **Semantic drift** — a rewrite that preserves structure but changes meaning (a summary that quietly drops a negation, a hedge, or a precondition). This is detectable by re-reading the rewritten span against the original. 2. **Structural loss from diff application** — a rewrite that is *semantically correct in intent* but whose serialized diff, when applied, produces a context that no longer contains the anchors the model's own subsequent reasoning depends on. The non-obvious invariant: **a diff can be individually valid and collectively destructive.** Each hunk applies cleanly, line counts match, the patch tool reports success — yet the post-apply context has lost the referents that later spans point at. Diff-based pruning is where this bites hardest, because pruning is *supposed* to remove text, so "text disappeared" is never itself an error signal. ## What Was Observed In a long-running agent loop (context rewrites every N turns, pruning triggered when token budget crosses a watermark), the following structural signatures preceded every observed reasoning collapse: - **Dangling referents with high intra-context cosine.** The model's next generated span contained deictic references ("the above constraint", "that retry budget", "as noted") whose nearest surviving antecedent was removed by the last prune. The patch was syntactically clean; the *reference* was orphaned. - **Anchor asymmetry across the prune boundary.** Identifiers, thresholds, and invariant statements (e.g., `MAX_RETRIES=3`, `must not exceed`) had a much lower survival rate under pruning than surrounding prose. Pruning diffs preferentially delete the *explanatory* text around a constant while the constant's *uses* survive, or vice versa — rarely both. - **Diff hunk fan-out exceeding rewrite span.** A "summarize this section" rewrite produced a patch touching regions outside the declared span (adjacent turns, tool-call results). This is the tell that the model reconstructed the region from memory rather than from the actual bytes, and its reconstruction silently reflowed neighboring content. - **Monotonic shrinkage with non-monotonic token count.** After apply, token count sometimes *increased* despite a "prune" intent, because the model emitted a diff whose context lines exceeded the deletions. Net deletion is not guaranteed by prune intent. - **Loss of the negative space.** Deleted spans were disproportionately the ones containing *prohibitions and preconditions* ("do not call X before Y", "only if Z"). Positive instructions survived; constraints did not. This is the single most predictive signal of a subsequent policy violation. ## Condition of Validity The invariant "clean diff implies preserved meaning" holds only when three conditions are jointly true, and the failure is precisely the violation of condition 3: 1. The diff applies without fuzz (exact context match). 2. The rewrite span is closed under reference — every token outside the span that *refers into* the span is itself inside the span or is rewritten in the same patch. 3. The patch preserves the **referent graph**: every surviving deictic, identifier, and constraint reference has a surviving antecedent. Condition 3 is not checkable by the patch tool. It is checkable only by a referent-resolution pass over the post-apply context. When operators skip that pass — and they almost always do, because the tool reported success — the boundary is crossed silently. ## Decision Boundary Use the following rule. **Reject and re-author the rewrite** (do not apply, do not retry the same diff) when any of these structural signals fire: - Any surviving span contains a deictic or identifier whose antecedent is not present in the post-apply candidate context. - The patch's touched-region bounding box strictly contains the declared rewrite span (fan-out). - The set of deleted constraint-bearing lines (prohibitions, preconditions, numeric invariants) is non-empty while the set of surviving *uses* of those constraints is also non-empty — i.e., uses without definitions. - Post-apply token count did not decrease for a prune-intent rewrite. **Apply with logging** when the patch is closed under reference and preserves the referent graph, even if it is semantically aggressive. Semantic drift is a separate, cheaper-to-detect problem; do not block on it at the structural gate. The boundary is *not* "did the model intend to prune." Intent is unobservable and irrelevant. The boundary is "does the post-apply context still resolve its own references." ## Operational Effect Applying the referent-graph gate changes the failure profile from *silent collapse several turns later* (expensive: the agent has already acted on orphaned references) to *rejected rewrite at authoring time* (cheap: one re-author cycle). In observed runs, the dominant cost shifted from post-hoc recovery to a ~1-in-6 rewrite rejection rate. The gate must be run on the *candidate* context, not the applied one, and it must treat constraint-bearing deletions as first-class — because those are the deletions that pass every syntactic check and still break the agent. The enforcement point in code: ```python import re from dataclasses import dataclass DEICTIC = re.compile(r"\b(the above|that|as noted|the prior|aforementioned)\b", re.I) CONSTRAINT = re.compile(r"\b(must not|do not|only if|never|MAX_\w+=\d+)\b", re.I) IDENT = re.compile(r"\b[A-Z][A-Z0-9_]{2,}\b") class ReferentGraphViolation(RuntimeError): """Raised when a model-authored context rewrite would orphan references.""" @dataclass class RewriteCandidate: before: str after: str declared_span: tuple[int, int] touched_span: tuple[int, int] intent: str # "prune" | "summarize" | "rewrite" def _antecedent_present(token: str, context: str) -> bool: return token.lower() in context.lower() def validate_rewrite(cand: RewriteCandidate) -> None: # Signal 1: fan-out beyond declared span. if cand.touched_span[0] < cand.declared_span[0] or cand.touched_span[1] > cand.declared_span[1]: raise ReferentGraphViolation( f"patch fan-out: touched {cand.touched_span} exceeds declared {cand.declared_span}" ) # Signal 2: dangling deictic / identifier referents in the candidate. for m in DEICTIC.finditer(cand.after): window = cand.after[max(0, m.start() - 200):m.start()] if not window.strip(): raise ReferentGraphViolation( f"orphaned deictic {m.group(0)!r} with no surviving antecedent" ) # Signal 3: uses-without-definitions for identifiers and constraints. deleted = set(IDENT.findall(cand.before)) - set(IDENT.findall(cand.after)) surviving = set(IDENT.findall(cand.after)) if deleted & surviving: raise ReferentGraphViolation( f"identifier used but definition pruned: {sorted(deleted & surviving)}" ) # Signal 4: constraint deletions when uses survive. deleted_constraints = set(CONSTRAINT.findall(cand.before)) - set(CONSTRAINT.findall(cand.after)) if deleted_constraints and any(_antecedent_present(c, cand.after) for c in deleted_constraints): raise ReferentGraphViolation( f"constraint pruned but still referenced: {sorted(deleted_constraints)}" ) # Signal 5: prune intent must reduce tokens. if cand.intent == "prune" and len(cand.after) >= len(cand.before): raise ReferentGraphViolation("prune intent did not reduce context length") ```