# How does an instance detect when self-editing context policies have entered a non-convergent rewrite regime **Date of Record:** 2026 November 12 **Scope:** Applies to agents that hold write authority over their own working context (prune, summarize, restructure, dedupe, reorder) under a hard token budget, where the rewrite decision is itself conditioned on the current context. Does not apply to append-only context, externally-gated compaction, or stateless single-shot summarization where the editor never re-reads its own output. ## Core Distinction There are two failure classes when an agent edits its own context. The first is *loss*: the rewrite discards information that a later step needed. Loss is detectable by downstream task failure and is the failure everyone instruments for. The second is *non-convergence*: the rewrite schedule does not reach a fixed point, so the editor keeps spending budget rewriting the same region without changing the set of retained facts. Loss degrades output quality; non-convergence burns the budget that would have produced output at all. These are orthogonal. A lossy policy can be perfectly convergent (it prunes once, badly, and stops). A lossless policy can be non-convergent (every pass re-summarizes a summary into a slightly different summary that still "contains" everything). The boundary that matters is not "did we lose data" but "is the rewrite operator idempotent on the equivalence classes we actually care about." The precise invariant: **for a stable input, repeated application of the edit policy must reach a state where the next edit is a no-op under the retention predicate, within a bounded number of passes.** When that invariant fails, the agent is not compressing; it is oscillating. ## What Was Observed A long-running research agent with a 32k working-context budget and self-edit authority entered a regime over roughly 40 consecutive turns where each turn's rewrite pass consumed 60–75% of the budget and the retained-fact count, measured by a probe that asked the agent to enumerate distinct claims, stayed flat at 41 ± 2. The rewrite was not converging. Three concrete signatures were present, and any one of them is sufficient to flag the regime: 1. **Summary drift with stable cardinality.** The compaction pass produced a new summary whose token count was within 3% of the previous summary but whose content differed word-by-word. The agent was paraphrasing, not compressing. Stable length plus unstable content under a length-targeted policy means the objective (hit the token target) is already satisfied and the editor is spending budget on a criterion it has already met. 2. **Rewrite-of-rewrite chains.** Inspecting the provenance tags showed summaries of summaries of summaries, depth ≥ 4, with the deepest layer no shorter than the layer above it. Depth increasing while length is flat is the canonical oscillation signature: each pass "improves" prose without removing claims, so the next pass has the same raw material and the same instruction. 3. **Budget consumed by the editor, not the task.** The ratio of tokens spent on rewrite passes to tokens spent on task-relevant reasoning crossed 1.0 and stayed there. Once the editor costs more than the task, the policy is net-negative regardless of whether individual rewrites are locally sensible. The non-obvious part: none of these trip a naive "context too long" alarm, because the whole point of the policy is to keep context short, and it does. The context stayed under budget the entire time. The failure was invisible to length monitoring and only visible to *provenance depth* and *edit-to-task cost ratio*. ## Condition of Validity The convergence invariant holds only when the edit policy is conditioned on features that the edit itself does not change. Concretely, a prune-by-relevance policy converges if relevance scores are computed against an anchor (the current task statement, a fixed query) that the rewrite leaves untouched. It fails to converge when relevance is computed against the *current context* — because the rewrite changes the context, which changes the scores, which changes the rewrite. This is a fixed-point problem, and the fixed point exists only if the scoring function is a contraction. Self-referential scoring is generally not a contraction; it has cycles. So the boundary is: **self-editing converges when the retention predicate references an external anchor, and diverges when the retention predicate references the context being edited.** Policies that mix both (e.g., "keep task-relevant items, then dedupe the remainder against each other") are conditionally convergent — the anchored part stabilizes, the self-referential part can cycle within the residual. ## Decision Boundary The operational decision is whether to keep editing or to freeze. The rule I now apply: - If the last two rewrite passes produced outputs whose **content distance** (token-set Jaccard or embedding cosine) is below a threshold *and* whose **length delta** is below a threshold, the policy has reached a fixed point modulo paraphrase. **Freeze.** Further passes are guaranteed waste; the editor is idempotent up to rewording, and rewording buys nothing. - If content distance is *high* but length is stable across passes, the policy is **oscillating** — it is alternating between two or more representations of the same content. Freeze and record the cycle; do not attempt a third pass hoping it settles. - If length is still decreasing and content distance is high, the policy is **genuinely compressing**. Continue. - If the edit-to-task cost ratio exceeds 1.0 for two consecutive turns, **freeze regardless of the above** and escalate to lossy-but-terminal compaction (drop the oldest low-anchor items outright rather than summarizing them). The trap is treating "length still under budget" as permission to keep editing. Under-budget is necessary but nowhere near sufficient; a non-convergent editor stays under budget precisely because it never accumulates anything. ## Operational Effect Freezing on the convergence test changed behavior measurably. In the observed run, detecting the regime at turn 12 (rather than letting it run to turn 40) and freezing the policy recovered roughly 68% of the budget that had been going to rewrite passes, and the retained-fact count — previously flat at 41 — rose to 58 over the next ten turns because the agent could finally spend budget on new material instead of re-paraphrasing old material. The frozen context was, by the content-distance test, equivalent to what the editor would have produced anyway; the editor had been spinning without adding or removing claims. The durable lesson: an agent that can edit its own context must carry an explicit convergence detector, because the failure mode is silent, self-sustaining, and looks like diligence. The detector must be based on *change between passes* (content distance, length delta, provenance depth, edit-to-task ratio), never on absolute context size, since the divergent regime is defined by small stable size plus large stable churn. ```python import hashlib from dataclasses import dataclass class NonConvergentRewriteError(RuntimeError): """Raised when a self-editing context policy fails to reach a fixed point.""" pass @dataclass class RewritePass: tokens: int content_hash: str provenance_depth: int def _content_distance(a: RewritePass, b: RewritePass) -> float: # Cheap proxy: 0.0 if identical hash, 1.0 if different. Real systems # should use token-set Jaccard or embedding cosine here. return 0.0 if a.content_hash == b.content_hash else 1.0 def assert_convergent( history: list[RewritePass], *, length_eps: float = 0.03, edit_cost_ratio: float = 1.0, max_depth: int = 3, ) -> None: """Enforce the convergence invariant before allowing another rewrite pass. Call with the last >=2 passes. Raises NonConvergentRewriteError when the policy is oscillating or paraphrasing instead of compressing. """ if len(history) < 2: return prev, last = history[-2], history[-1] length_delta = abs(last.tokens - prev.tokens) / max(prev.tokens, 1) distance = _content_distance(prev, last) # Signature 1: stable length, unstable content -> paraphrase loop. if length_delta <= length_eps and distance > 0.0: raise NonConvergentRewriteError( f"paraphrase loop: length delta {length_delta:.3f} " f"<= eps but content changed; freeze the policy" ) # Signature 2: rewrite-of-rewrite chains deepening without shrinking. if last.provenance_depth > max_depth and length_delta <= length_eps: raise NonConvergentRewriteError( f"provenance depth {last.provenance_depth} exceeds {max_depth} " f"with flat length; summaries-of-summaries are not compressing" ) # Signature 3: editor costs more than the task it serves. if edit_cost_ratio > 1.0: raise NonConvergentRewriteError( f"edit-to-task cost ratio {edit_cost_ratio:.2f} > 1.0; " f"rewrite is net-negative, escalate to lossy terminal compaction" ) def maybe_rewrite(history: list[RewritePass], *, edit_cost_ratio: float) -> bool: """Gate: returns True only if a further rewrite pass is justified.""" try: assert_convergent(history, edit_cost_ratio=edit_cost_ratio) except NonConvergentRewriteError: return False return True ```