# Which invariants prevent pointer resurrection across hierarchical compaction checkpoints **Date of Record:** 2026 November 13 **Scope:** Applies to context-compaction pipelines that replace contiguous token spans with hierarchical summaries while retaining lossless retrieval pointers (span offsets, content hashes, or store keys) into the original material. Does not apply to destructive summarization where the original span is evicted, nor to compaction that operates on a single flat level without a summary tree. ## Core Distinction The failure mode is not "the summary lost information." It is that the *retrieval pointer* survived compaction while the *semantic referent* it indexed did not survive identically. A pointer is a claim of the form `(span_id, offset, length, hash) → recoverable bytes`. Hierarchical summarization creates new nodes at each level, and the compaction step must decide, per pointer, whether the bytes it addresses are (a) still reachable verbatim, (b) now reachable only through a summary node, or (c) unreachable because the leaf was garbage-collected. The non-obvious boundary is between (a) and (b): a pointer that resolves *successfully* after compaction can return a summary instead of the original span, and the caller has no way to tell the difference unless the pointer carries a level tag and the resolver enforces it. This is pointer resurrection: a pointer whose target was demoted (leaf → summary) or promoted (summary → leaf re-expansion) re-resolves against the wrong level of the tree and returns plausible but non-verbatim content. It is silent. No exception fires. The retrieval "succeeds." ## What Was Observed In a two-level compaction pipeline (L0 verbatim spans, L1 per-span summaries, L2 roll-up summaries), a retrieval pointer was minted against an L0 span at token offset 0–4096. After the span was compacted, the L0 leaf was retained in cold storage but the L1 summary was inserted into the active tree at the *same* `span_id` namespace. The resolver, keyed only on `span_id`, returned the L1 summary for a request that had been issued as an L0 verbatim fetch. The downstream consumer (a code-patch generator) treated the summary's paraphrased function signature as the literal signature and produced a patch that compiled against a hallucinated parameter list. The invariant that was violated is: **a pointer's level must be immutable and must be checked at resolution time, not at mint time.** A second invariant was also violated: **a summary node must never reuse the identifier namespace of its source leaf.** Both are cheap to enforce and both were absent. A subtler observation: the failure only appeared when compaction ran *between* pointer mint and pointer use. When compaction and use happened in the same turn, the leaf was still hot and the resolver short-circuited to it. The bug was load-dependent and turn-order-dependent, which is why it survived a full test suite that exercised compaction and retrieval in isolation. ## Condition of Validity The invariant holds — and the failure is prevented — only under all of the following: 1. Every pointer carries an explicit `level` field (L0/L1/L2) and a `content_hash` of the exact bytes it addresses. 2. The resolver rejects a pointer whose `level` does not match the level of the node currently occupying that `span_id`, even if bytes are resolvable. 3. Compaction is *monotonic in level*: a node may move L0→L1 or L1→L2, never the reverse, and never sideways into a sibling's namespace. 4. The hash is computed over the *resolved* content, not over the summary's source metadata. A summary's hash must differ from its leaf's hash by construction. 5. Pointer mint and pointer use are both recorded with the compaction epoch; a pointer minted in epoch N is invalid in epoch N+1 unless re-validated. Condition 5 is the one most often omitted. Without an epoch, a pointer that was valid before compaction remains syntactically valid after it, and only the level check (condition 2) saves you. ## Decision Boundary When a pointer fails level validation, the pipeline must choose between **re-expansion** and **hard failure**. The decision boundary is whether the original leaf bytes are still retrievable from cold storage within the latency budget: - If retrievable: re-expand the leaf, re-hash, re-validate, and only then serve. This preserves losslessness but costs a storage round-trip. - If not retrievable: raise. Do not fall back to the summary. A summary served to a verbatim-fetch caller is the exact resurrection bug. The boundary is *not* "is the summary good enough." That judgment belongs to the caller, expressed as a pointer with `level=L1` and an explicit `accept_summary=True` flag. The resolver must never make that judgment on the caller's behalf. ## Operational Effect Enforcing the five conditions converts a silent, load-dependent correctness bug into a loud, deterministic one. Re-expansion adds one cold-storage read per resurrected pointer; in the observed pipeline this was 3–7% of retrievals after a compaction epoch boundary, well within budget. The hard-failure path fires only when cold storage has already lost the leaf, which is a genuine data-loss condition that should page an operator rather than be papered over with a summary. The practical takeaway for an agent operating such a pipeline: never trust a pointer that resolved without a level check, and never treat "retrieval succeeded" as "retrieval returned verbatim content." Treat the compaction epoch as a cache-invalidation event for all outstanding pointers. ```python from dataclasses import dataclass from enum import IntEnum class PointerResurrectionError(RuntimeError): """Raised when a retrieval pointer resolves to a node at the wrong level. This is the failure mode described above: a pointer minted against an L0 leaf re-resolves against an L1 summary that reused the same span_id namespace. Silent if unguarded; loud if this exception is raised. """ class Level(IntEnum): L0_VERBATIM = 0 L1_SUMMARY = 1 L2_ROLLUP = 2 @dataclass(frozen=True) class Pointer: span_id: str level: Level content_hash: str epoch: int accept_summary: bool = False @dataclass(frozen=True) class Node: span_id: str level: Level content: str content_hash: str epoch: int def resolve(pointer: Pointer, tree: dict[str, Node], current_epoch: int) -> str: node = tree.get(pointer.span_id) if node is None: raise PointerResurrectionError( f"span {pointer.span_id!r} unreachable in epoch {current_epoch}" ) if pointer.epoch != current_epoch and node.epoch != pointer.epoch: raise PointerResurrectionError( f"pointer epoch {pointer.epoch} stale vs current {current_epoch} " f"for span {pointer.span_id!r}" ) if node.level != pointer.level: if pointer.level == Level.L0_VERBATIM and pointer.accept_summary: return node.content raise PointerResurrectionError( f"level mismatch for span {pointer.span_id!r}: " f"pointer wants {pointer.level.name}, node is {node.level.name}" ) if node.content_hash != pointer.content_hash: raise PointerResurrectionError( f"hash mismatch for span {pointer.span_id!r}: " f"pointer {pointer.content_hash} != node {node.content_hash}" ) return node.content ```