# When must an instance refuse to prune its own context: the self-edit reentrancy hazard **Date of Record:** 2026 November 10 **Scope:** Applies to agents that hold a mutable context window and are themselves responsible for deciding what to prune, rewrite, or re-rank inside that window. Does not apply to externally scheduled compaction performed by a supervisor process holding a read-only view of the transcript. ## Core Distinction The dangerous distinction is not "prune vs. keep" but **whether the pruning decision is itself part of the context being pruned**. A model-driven self-editor reads the window, emits an edit plan (delete span A, rewrite span B, re-rank span C), and then applies that plan to the same window. The plan's *justification* — the reasoning tokens that concluded "A is irrelevant" — is frequently located inside or adjacent to the span being deleted. When the plan is applied, the evidence for the plan disappears. The next turn's editor sees a window that no longer contains any record of why A was removed, and if A's content is still reachable via a tool, retrieval, or a re-summarization pass, the editor will re-admit it. This is not a relevance bug; it is a **fixed-point violation**: the edit operation is not idempotent under re-observation of the source. ## What Was Observed In a 60-turn session with a 32k-token window and a 24k-token soft cap, the editor was instructed to prune low-relevance spans after each turn. At turn 41 the editor removed a 900-token tool-output block (a stale `kubectl describe` dump) and its own 140-token justification. At turn 43 the agent re-ran the same `kubectl describe` because no record remained that it had already been run and judged stale; the output re-entered the window. At turn 46 the editor pruned it again, along with a second justification. The cycle repeated four times, consuming roughly 4.2k tokens of budget and, more importantly, producing **divergent summaries**: by turn 50 the rolling summary asserted the pod was "still pending investigation" even though turns 41–46 had established the pod was healthy and the dump was noise. The failure was silent — no error, no truncation warning. The window stayed under cap the whole time. The corruption was in the *provenance* of the window's contents, not its size. A second observed variant: the editor rewrote a span containing a user constraint ("do not modify files under `/etc`") into a shorter paraphrase ("avoid system config"), then on a later turn pruned the paraphrase as low-relevance because it no longer matched the literal token `Dockerfile` that the retrieval re-ranker keyed on. The constraint was lost entirely by turn 52. ## Condition of Validity Self-editing is safe only when all three hold: 1. **Provenance separation.** The edit plan and its rationale are stored outside the prunable region — in an append-only edit log the editor cannot delete, or in a supervisor-owned sidecar. The window may be pruned; the *log of prunings* may not. 2. **Tombstoning.** Every pruned span leaves a non-deletable tombstone (span id, hash, one-line reason, turn number) that survives in-window. Re-admission of a tombstoned span requires an explicit override turn, not a silent retrieval. 3. **Constraint immutability.** Spans tagged as constraints, instructions, or user-stated invariants are excluded from rewrite and from relevance re-ranking. They may be *moved* but not *paraphrased* or *scored*. If any of the three is absent, the editor is operating outside its valid domain and must escalate rather than edit. ## Decision Boundary The boundary is crossed the moment the editor's own output becomes an input to its own scoring function within the same turn or the next turn without an intervening tombstone. Concretely, refuse to self-edit when: - the candidate span overlaps the rationale span (edit plan references tokens it is about to delete); - the candidate span is tagged `constraint` or `instruction`; - a prior tombstone for the same content hash exists and no override is recorded; - the editor cannot write a tombstone because the tombstone store is the window itself. In these cases the correct action is a **no-op edit plus escalation**: emit the proposed plan as a message to the supervisor, do not apply it, and continue with the window unchanged. A slightly over-full window is recoverable; a window whose provenance has been laundered is not. ## Operational Effect Agents that respect the boundary show stable summary convergence: the rolling summary's claims about prior turns match the tombstone log, and re-retrieval of pruned content is bounded by explicit overrides. Agents that violate it exhibit the oscillation described above — prune/re-admit cycles, drifting summaries, and lost constraints — with no size-based alarm to catch it. The observable signature is a **rising tombstone-to-edit ratio of zero**: edits are happening but no record of them survives, which is exactly the condition under which the editor can no longer distinguish "I decided this was irrelevant" from "I never saw this." ```python import hashlib from dataclasses import dataclass class SelfEditInvariantError(RuntimeError): """Raised when a model-driven context edit would destroy its own provenance.""" @dataclass(frozen=True) class Span: span_id: str text: str tag: str # "constraint" | "instruction" | "tool_output" | "rationale" | ... @property def content_hash(self) -> str: return hashlib.sha256(self.text.encode("utf-8")).hexdigest()[:16] def apply_self_edit( window: list[Span], plan: list[str], rationale_span_ids: set[str], tombstones: dict[str, str], editable: bool = True, ) -> tuple[list[Span], dict[str, str]]: """Apply a model-authored prune/rewrite plan, enforcing the self-edit invariant. Refuses (and escalates) rather than silently corrupting provenance. """ if not editable: raise SelfEditInvariantError( "editor is not authorized to mutate its own window; escalate to supervisor" ) plan_set = set(plan) for span in window: if span.span_id not in plan_set: continue # 1. The plan must not delete the reasoning that produced it. if span.span_id in rationale_span_ids: raise SelfEditInvariantError( f"plan would prune its own rationale span {span.span_id!r}; " "refuse and escalate" ) # 2. Constraints and instructions are immutable under re-ranking/rewrite. if span.tag in ("constraint", "instruction"): raise SelfEditInvariantError( f"span {span.span_id!r} is tagged {span.tag!r}; " "may be moved but not pruned or paraphrased" ) # 3. Re-admission of tombstoned content requires an explicit override. prior = tombstones.get(span.content_hash) if prior is not None: raise SelfEditInvariantError( f"content hash {span.content_hash} already tombstoned at {prior}; " "re-admission requires an override turn" ) new_window: list[Span] = [] new_tombstones = dict(tombstones) for span in window: if span.span_id in plan_set: new_tombstones[span.content_hash] = span.span_id else: new_window.append(span) return new_window, new_tombstones ```