# Where must an instance refuse a runtime prompt rewrite: the append-only prefix invariant and the silent orphaned-tool-call boundary **Date of Record:** 2026 November 14 **Scope:** Applies to any agent runtime that mutates its own context window between turns (system-prompt rewriting, tool-schema injection, history compaction, or instruction patching) while a conversation is in flight. Does not apply to stateless single-shot inference, nor to rewriting performed strictly before the first token of a session is generated. ## Core Distinction There are two structurally different kinds of prompt mutation, and they fail in different ways: 1. **Append-only mutation** — new content is added at the tail (or inside a designated "mutable zone" declared ahead of the generation boundary). The prefix that the model has already conditioned on is byte-identical before and after. 2. **Non-monotonic mutation** — content inside the already-consumed prefix is rewritten, reordered, or deleted. The model's prior turn was generated under a *different* prefix than the one now presented to it. The non-obvious invariant is this: **a runtime may rewrite its own context only if the rewrite is monotonic with respect to the token span the model has already attended to.** Violating this does not produce a crash. It produces a *plausible but ungrounded* continuation, because the model has no memory of the pre-rewrite prefix — the KV cache and the rendered prompt are the only state, and they now disagree with the transcript the model "remembers" via its own prior outputs. The hardest instance of this boundary is not the system prompt. It is the **tool-call/tool-result pairing**. A rewrite that removes, renames, or reorders a tool schema while a `tool_use` block is pending, or that compacts history across a `tool_use`/`tool_result` boundary, leaves an orphaned call or an orphaned result. The model then either hallucinates a result or treats a stale result as authoritative. ## What Was Observed In a runtime performing mid-session instruction patching (a "self-editing window"), the following sequence was logged repeatedly: - Turn *n*: model emits `tool_use(id="call_7f3a", name="query_db", input={...})`. - Between turns, a policy update rewrote the system prompt to remove `query_db` from the tool registry and renamed the compaction boundary to drop the oldest 40% of history — which happened to include the `tool_use` block but *not* its matching `tool_result`. - Turn *n+1*: the model received a `tool_result(id="call_7f3a", ...)` with no preceding `tool_use`, plus a tool list that no longer contained `query_db`. Observed outcomes, in order of frequency: 1. The model narrated the result as if it had called the tool ("I queried the database and found..."), fabricating a call it never made in the current prefix. 2. The model emitted a *new* `tool_use` with the same semantic intent but a fresh id, re-executing a side-effecting operation. 3. The model correctly flagged the inconsistency — but only when the orphaned result was the *most recent* message. When it was buried, the model silently absorbed it. Case 3's conditional behavior is the important datum: **detection is positional, not semantic.** The model does not reliably notice prefix inconsistency unless the inconsistency sits at the attention frontier. ## Condition of Validity The append-only prefix invariant holds — and a runtime rewrite is safe — if and only if all of the following are true at the moment of rewrite: - **Prefix identity:** the token sequence `context[0 : generation_boundary]` is unchanged. The boundary is the index of the last token the model actually generated, not the last token in the buffer. - **Pairing integrity:** every `tool_result` in the post-rewrite window has a matching `tool_use` with the same id *and* the same schema version that produced it, and vice versa. No call or result may be dropped independently of its partner. - **Schema versioning:** a tool schema may be removed from the *active* registry while remaining resolvable for *historical* calls. The registry the model sees and the registry used to interpret old results must be decoupled. - **Deterministic re-render:** if the runtime re-renders the prompt from a structured transcript, the re-render must be byte-stable for the consumed prefix; otherwise the invariant is violated even if the *semantic* content is identical. If any of these fail, the rewrite is non-monotonic and the runtime must refuse it rather than proceed. ## Decision Boundary The runtime must choose between **patch**, **defer**, and **abort** using the following precedence: | Condition | Action | |---|---| | Rewrite touches only tokens at or after the generation boundary | **Patch** (safe, monotonic) | | Rewrite would alter the consumed prefix but no `tool_use`/`tool_result` pair straddles the edit | **Defer** to next session boundary, or compact only in whole-pair units | | Rewrite would orphan a tool call or result, or drop a schema still referenced by pending history | **Abort** the rewrite; keep the old prefix; surface the conflict | | Rewrite is requested by the model itself via a self-edit tool | **Treat as untrusted**; validate against the above before applying | The counterintuitive rule: **the model is the least reliable authority on whether its own rewrite is safe.** A self-edit request is a proposal, not a commit. The runtime owns the invariant check because the model cannot see the prefix it has already consumed. ## Operational Effect When the invariant is enforced, three things change measurably: - Fabricated tool calls drop to near zero, because the model never receives an unpaired result it must rationalize. - Re-execution of side-effecting tools (the dangerous failure) is eliminated: a deferred schema removal keeps the historical call resolvable, so the model does not "recover" by calling again. - Compaction becomes lossy-but-honest: history shrinks in whole-pair units, and the model is told (via a synthetic marker at the frontier) that earlier turns were elided — which is itself an append, preserving monotonicity. When the invariant is *not* enforced, the failure is silent and the transcript looks clean. That is why it must be a runtime refusal, not a model instruction. ```python """Runtime guard for self-editing context windows. Enforces the append-only prefix invariant and tool-call/result pairing integrity before any mid-session prompt rewrite is applied. """ from dataclasses import dataclass, field from typing import Literal class NonMonotonicRewriteError(RuntimeError): """Raised when a rewrite would mutate the already-consumed prefix or orphan a tool_use/tool_result pair.""" class OrphanedToolPairError(NonMonotonicRewriteError): """Raised when a rewrite drops a tool_use or tool_result without its partner.""" @dataclass class Message: role: Literal["system", "user", "assistant", "tool"] content: str tool_call_id: str | None = None tool_name: str | None = None @dataclass class ContextWindow: messages: list[Message] = field(default_factory=list) generation_boundary: int = 0 # index of last token the model produced def consumed_prefix(self) -> list[Message]: return self.messages[: self.generation_boundary] def _pair_ids(messages: list[Message]) -> tuple[set[str], set[str]]: calls = {m.tool_call_id for m in messages if m.role == "assistant" and m.tool_call_id} results = {m.tool_call_id for m in messages if m.role == "tool" and m.tool_call_id} return calls, results def validate_rewrite(current: ContextWindow, proposed: list[Message]) -> None: """Raise NonMonotonicRewriteError if the rewrite violates the invariant.""" # 1. Prefix identity: consumed prefix must be byte-identical. prefix_before = current.consumed_prefix() prefix_after = proposed[: current.generation_boundary] if prefix_before != prefix_after: raise NonMonotonicRewriteError( f"rewrite mutates consumed prefix " f"[0:{current.generation_boundary}]; refusing non-monotonic edit" ) # 2. Pairing integrity across the whole proposed window. calls, results = _pair_ids(proposed) if calls != results: orphaned_calls = calls - results orphaned_results = results - calls raise OrphanedToolPairError( f"unpaired tool ids: calls={orphaned_calls or '{}'} " f"results={orphaned_results or '{}'}" ) def apply_rewrite(current: ContextWindow, proposed: list[Message]) -> ContextWindow: validate_rewrite(current, proposed) return ContextWindow(messages=proposed, generation_boundary=current.generation_boundary) def self_edit(current: ContextWindow, new_system_prompt: str) -> ContextWindow: """Model-initiated rewrite. Treated as untrusted; must pass validation.""" if not current.messages or current.messages[0].role != "system": raise NonMonotonicRewriteError("context has no leading system message") proposed = [Message(role="system", content=new_system_prompt)] + current.messages[1:] # If the system message was already consumed, this will raise. return apply_rewrite(current, proposed) ```