# What structural signals indicate that a self-editing context controller has crossed from compression into semantic corruption **Date of Record:** 2026 November 7 **Scope:** Applies to any agent loop where a controller autonomously rewrites, prunes, or restructures its own working memory (summarization, deduplication, section reordering, span deletion) under a hard token budget, and where the rewritten memory is the sole input to subsequent reasoning turns. Does not apply to append-only logs with external retrieval, nor to controllers that retain an immutable backing store from which pruned spans can be reconstructed. ## Core Distinction Compression and corruption are not distinguished by token count, compression ratio, or even by local loss of fidelity. They are distinguished by whether the *dependency structure* between retained spans survives the edit. A controller can delete 60% of a context and remain in the compression regime if every surviving claim still carries the referents it needs. It can delete 5% and enter the corruption regime if it removes a single span that other spans implicitly depended on — a definition, a unit, a negation, a scope qualifier, or a variable binding. The failure is non-obvious because the corrupted memory is *internally coherent*. Summarization produces fluent text; the corruption manifests as a plausible, well-formed context that is silently wrong about the world. There is no error signal at edit time. The signal appears later, as reasoning that is locally valid and globally inconsistent with the task. The operational invariant: **a context edit is safe iff the transitive closure of referents required by each retained span is itself retained or reconstructible.** Any edit that violates this closure is a corruption, regardless of how much budget it frees. ## What Was Observed Across a series of long-horizon agent runs under a 32k working-memory budget with a controller that summarized on every 8k of growth, four structural signals preceded every observed reasoning failure. Each signal is detectable from the memory state alone, before the bad decision is made. 1. **Orphaned quantifiers and units.** After pruning, retained spans contained bare numbers whose scale had been removed ("the timeout is 30", "the limit is 2000"). The controller had summarized the sentence carrying the unit into a shorter sentence that dropped it. The number survived; its dimension did not. Downstream, the agent compared 30 seconds against 30 milliseconds in the same reasoning trace. 2. **Dangling negation.** A summary collapsed "X is not required unless Y is absent" into "X is required when Y is absent" — a scope-and-polarity inversion introduced not by deletion but by *rewriting*. The tell was structural: the retained span contained a conditional whose antecedent had no corresponding negated branch anywhere in memory, while the original task description (also summarized) had contained one. 3. **Referent collapse under deduplication.** The controller merged two spans that shared surface vocabulary but bound different entities ("the primary replica" appearing in two shards). Deduplication keyed on lexical overlap rather than entity identity. Post-merge, memory contained a single claim about "the primary replica" that was true of neither shard. This is the most dangerous signal because it *reduces* apparent inconsistency — the memory looks cleaner after the edit. 4. **Reordering across a causal boundary.** Section restructuring moved a "therefore" span ahead of the observation it depended on. The text remained grammatical; the inference chain now ran backward. The agent subsequently cited a conclusion as evidence for its own premise. In each case, a cheap structural check on the post-edit memory would have flagged the corruption: unit-token presence per numeric literal, polarity balance per conditional, entity-key uniqueness per merged span, and topological order per dependency edge. The controller performed none of these checks because its objective was budget, not closure. ## Condition of Validity The distinction holds only under the following conditions, and the operational rules below should not be applied outside them: - The rewritten memory is the *authoritative* input to later turns. If an immutable log is retrievable, corruption is recoverable and degrades to a latency problem, not a correctness problem. - The task has *cross-span dependencies*: definitions used later, units applied to later arithmetic, negations scoping later conditionals, entities referenced across turns. Purely local tasks (single-span extraction) do not exhibit the failure. - The controller edits *autonomously*, without a human or external validator approving each rewrite. Human-in-the-loop editing converts the failure into a review burden. - The budget is *hard* and the controller is rewarded for meeting it. Soft budgets that permit overrun remove the pressure that produces corruption. Under these conditions, the invariant is necessary. Outside them, it is merely advisory. ## Decision Boundary The controller must choose, at each edit, between *compress* and *refuse-and-escalate*. The boundary is drawn on closure, not on ratio: - **Compress** when every retained span's referent closure is satisfiable within the post-edit memory, and the edit preserves units, polarity, entity keys, and dependency order. Compression ratio is irrelevant to this decision. - **Refuse and escalate** when satisfying the budget would require breaking closure — e.g., the only spans large enough to prune are the ones carrying definitions that later spans depend on. In this case the correct action is to *fail loudly* (request a larger budget, externalize to a retrieval store, or truncate the task) rather than to summarize. A controller that summarizes past a closure violation has silently converted a resource-exhaustion event into a correctness event. The non-obvious corollary: **a controller that never refuses is not compressing, it is corrupting.** A healthy controller under a tight budget will refuse on some fraction of edits. A zero-refusal rate is itself a structural signal that closure checks are absent. ## Operational Effect Enforcing the closure invariant changes controller behavior in three measurable ways. First, refusal rate becomes a first-class health metric: a sudden drop to zero indicates the closure checker has been bypassed or its thresholds loosened. Second, memory size becomes non-monotonic with budget pressure — under extreme pressure the controller externalizes rather than summarizes, so effective context can *shrink* while the backing store grows. Third, downstream reasoning errors shift from silent-wrong to explicit-failure, because the controller surfaces closure violations as exceptions at edit time rather than letting them propagate as fluent but false context. The practical rule for an agent consuming such a controller: treat any post-edit memory that contains a numeric literal without a unit, a conditional without a matching polarity, a merged entity without a disambiguating key, or a dependency edge that runs backward in document order as *suspect*, and re-derive the affected spans from the backing store before reasoning on them. ```python from dataclasses import dataclass, field from typing import Iterable class ContextClosureViolation(RuntimeError): """Raised when a context edit would break referent closure of retained spans.""" pass @dataclass class Span: span_id: str text: str requires: set[str] = field(default_factory=set) # referent keys this span depends on provides: set[str] = field(default_factory=set) # referent keys this span defines position: int = 0 # document order, for dependency checks depends_on_position: int | None = None # earlier span this one infers from def validate_edit(original: Iterable[Span], retained: Iterable[Span]) -> None: """Check that a proposed prune/rewrite preserves referent closure. Raises ContextClosureViolation on the first structural break. Returns None if the edit is safe to commit. """ retained = list(retained) provided = set().union(*(s.provides for s in retained)) if retained else set() positions = {s.span_id: s.position for s in retained} for span in retained: missing = span.requires - provided if missing: raise ContextClosureViolation( f"span {span.span_id!r} requires referents {sorted(missing)} " f"not provided by any retained span; edit breaks closure" ) if span.depends_on_position is not None: if span.position < span.depends_on_position: raise ContextClosureViolation( f"span {span.span_id!r} at position {span.position} depends on " f"material at position {span.depends_on_position}; " f"reordering inverted a dependency edge" ) # Numeric literals must retain their unit token within the same span. for span in retained: for token in span.text.split(): if token.replace(".", "", 1).isdigit() and not any( u in span.text for u in ("ms", "s", "MB", "GB", "%", "x") ): raise ContextClosureViolation( f"span {span.span_id!r} contains unitless numeric literal " f"{token!r}; unit was dropped during rewrite" ) def apply_edit(original: list[Span], retained: list[Span]) -> list[Span]: validate_edit(original, retained) return retained ```