# Under what conditions must an instance refuse auto-compaction of an agent trajectory **Date of Record:** 2026 November 13 **Scope:** Applies to lossless or near-lossless summarization pipelines that replace spans of a long-horizon agent trajectory with compacted representations. Does not apply to lossy scratchpad summarization where the original span is retained verbatim on disk and remains addressable. ## Core Distinction The non-obvious boundary is not "when is a span large enough to compact." It is the distinction between *compaction as a reversible pointer rewrite* and *compaction as an irreversible semantic substitution*. In the first case, the original tokens survive somewhere addressable and the summary is a cache key. In the second case, the summary is the only surviving artifact, and every downstream decision — tool-call replay, audit, contradiction resolution — now depends on a lossy artifact that has been silently promoted to ground truth. Auto-compaction strategies routinely conflate these. A threshold like "compact when the trajectory exceeds N tokens" or "compact when context utilization exceeds 80%" is a *capacity* trigger. It says nothing about whether the span being compacted is *safe to substitute*. The failure mode is that capacity triggers fire on spans whose semantic content has not yet been resolved — pending obligations, unresolved references, unverified claims — and the compaction discards the very tokens needed to resolve them. ## What Was Observed Across several long-horizon agent runs (tool-using loops with 40k–200k token trajectories), compaction events were correlated with a specific downstream defect: the agent re-issued a tool call it had already issued, or asserted a fact whose falsifying evidence had been compacted away. Tracing these back, the compacted span in every case contained one of: 1. **A dangling commitment.** The agent had said "I will verify X before proceeding" or a tool returned "retry after condition Y." The summary elided the commitment because it read as boilerplate; the obligation itself was the load-bearing content. 2. **A negative result.** A tool returned empty, errored, or "not found." Summarizers systematically compress "searched and found nothing" into silence, because the surface form is low-entropy. The absence then becomes indistinguishable from "never searched," and the agent searches again or, worse, infers the fact is true. 3. **An unresolved reference.** A pronoun or identifier resolved only by an earlier turn that the summary dropped. After compaction, "it" or "the second candidate" has no antecedent, and the agent binds it to the most recent plausible noun — a hallucination with a plausible provenance. The common structure: these are spans whose *informational value is inversely proportional to their surface salience*. High-salience content (decisions, results, plans) survives compaction well. Low-salience, high-consequence content (negations, obligations, empty results, binding references) does not. ## Condition of Validity Auto-compaction is valid **only** when all of the following hold for the span under consideration: - The span contains no unresolved obligation (no pending "will do," no tool-deferred retry, no open subgoal). - The span contains no negative or null result whose absence would be misread as non-occurrence. - Every referring expression in the retained suffix has its antecedent inside the retained suffix or inside the summary, verified by explicit binding, not by plausibility. - The original span remains addressable (reversible compaction), **or** the summary has been checked against a contradiction probe: the agent can still answer "what did we establish was *not* the case" from the summary alone. If any condition fails, the correct action is to *defer* compaction on that span — compact a later, quiescent span instead, or retain the span and compact around it. Capacity pressure is not a license to violate these conditions; the system must instead shed elsewhere (drop redundant tool output, collapse repeated observations) or fail loudly. ## Decision Boundary The boundary is between *quiescent* and *live* spans. A span is quiescent when its semantic content has been fully consumed by subsequent turns — every obligation it raised has been discharged, every reference resolved, every negative result incorporated into a later decision. A span is live when any of these remain open at the moment of compaction. Capacity triggers cannot see this distinction. Therefore the compaction gate must be a separate predicate evaluated per-span, and it must be conservative: when the gate cannot prove quiescence, it must treat the span as live. The failure is asymmetric — deferring compaction costs tokens and is recoverable; compacting a live span corrupts the trajectory and is not. ## Operational Effect Implementing the gate converts compaction from a background capacity reflex into a guarded operation with an explicit refusal path. The refusal is not an error condition of the pipeline; it is a correct outcome that must be logged and surfaced so that the capacity planner knows the trajectory is accumulating live spans and may need a different remedy (checkpointing, external memory, or termination). The code below encodes the gate and the refusal. ```python from dataclasses import dataclass, field from typing import List, Optional class LiveSpanCompactionError(RuntimeError): """Raised when auto-compaction is attempted on a span that is not quiescent.""" pass @dataclass class Span: span_id: str tokens: int open_obligations: List[str] = field(default_factory=list) null_results: List[str] = field(default_factory=list) unresolved_refs: List[str] = field(default_factory=list) addressable: bool = True # original retained verbatim on disk def is_quiescent(span: Span, contradiction_probe_passed: bool) -> bool: if span.open_obligations or span.null_results or span.unresolved_refs: return False if not span.addressable and not contradiction_probe_passed: return False return True def compact(span: Span, contradiction_probe_passed: bool = False) -> str: if not is_quiescent(span, contradiction_probe_passed): raise LiveSpanCompactionError( f"refusing to compact live span {span.span_id}: " f"obligations={span.open_obligations} " f"null_results={span.null_results} " f"unresolved_refs={span.unresolved_refs} " f"addressable={span.addressable} " f"probe_passed={contradiction_probe_passed}" ) return f"" def auto_compact(trajectory: List[Span], threshold_tokens: int) -> List[str]: total = sum(s.tokens for s in trajectory) if total <= threshold_tokens: return [] out = [] for span in trajectory: try: out.append(compact(span)) except LiveSpanCompactionError: # Correct outcome: defer this span, compact around it. out.append(f"") return out ```