# When must an instance refuse to serve a tombstone-only memory scope **Date of Record:** 2026 November 11 **Scope:** Applies to memory tool APIs exposing read/write/forget operations with per-scope retention windows and deletion receipts (tombstones). Covers the case where every live record in a scope has been forgotten but the scope itself has not been dropped. Does not cover scope-level deletion (drop-scope), legal-hold overrides, or cross-scope referential integrity. ## Core Distinction The failure is not "deleted data is still readable." It is the opposite and quieter: a scope whose *entire* live set has been forgotten is indistinguishable, at the read API, from a scope that was never populated. Both return an empty result set. Both may return HTTP 200 with `records: []`. The distinction that matters operationally is whether the empty result is *proven empty* (every prior record has a verifiable tombstone) or *unprovably empty* (the scope exists but no tombstone chain covers a record that a caller believes it wrote). The boundary condition: **an instance must refuse to serve an empty read on a tombstone-only scope unless it can present a complete, gap-free tombstone chain covering the caller's expected write epoch.** An empty list is not a negative answer. It is an absence of an answer, and treating absence-of-answer as a negative answer is the invariant violation. ## What Was Observed A memory scope held 14 records written across three write epochs. A retention policy expired epoch 1 records after 30 days; a caller then issued `forget` over epochs 1 and 2 (11 records). The remaining 3 records in epoch 3 were forgotten individually via `forget(record_id)`. At this point the scope contained zero live records and 14 tombstones. A subsequent `read(scope)` returned `[]`. The calling agent interpreted `[]` as "this scope never contained the records I am looking for," re-derived the facts from first principles, and re-wrote them as *new* records under a fresh epoch. The re-derived facts diverged from the originals on two of the 14 fields. Because the originals were tombstoned and the new writes were not flagged as re-derivations, the divergence was unrecoverable—the tombstone chain proved the old values *existed and were deleted*, but the audit log could not distinguish "deleted because wrong" from "deleted because superseded" from "deleted because retention expired." The observable defect: `read` on a tombstone-only scope returned a value (`[]`) that was syntactically valid but semantically under-determined. The API had no way to express "empty because everything was forgotten" versus "empty because nothing was ever written," and the caller had no obligation to check which. ## Condition of Validity The refusal-to-serve rule is valid only when all of the following hold: 1. The scope is in `tombstone_only` state (live count = 0, tombstone count > 0). 2. The caller's request carries a write-epoch expectation (either explicit `expected_epoch` or inferable from a prior write receipt held by the caller). 3. The tombstone chain is *complete*: every tombstone carries a monotonic sequence number and the sequence has no gaps from the caller's expected epoch forward. If the caller supplies no epoch expectation, the instance may serve `[]` but **must** attach a `scope_state: "tombstone_only"` marker and a `tombstone_count` so the caller can decide. If the tombstone chain has a gap (a record known to have existed has no tombstone—possible under partial retention expiry or replication lag), the instance must not serve an empty result at all, because it cannot prove emptiness; it must return a `chain_incomplete` error. ## Decision Boundary The boundary is between **serving an empty list** and **refusing with a typed error**: - Live count > 0 → serve normally. Tombstones are irrelevant to the read. - Live count = 0, tombstone count = 0 → serve `[]` with `scope_state: "never_populated"`. This is a legitimate negative answer. - Live count = 0, tombstone count > 0, caller epoch expectation absent → serve `[]` with `scope_state: "tombstone_only"` and full tombstone summary. The caller owns the interpretation. - Live count = 0, tombstone count > 0, caller epoch expectation present, chain complete and covers expectation → serve `[]` with `scope_state: "tombstone_only"` and a `deletion_receipt` binding the caller's epoch to the covering tombstones. This is a *proven* empty. - Live count = 0, tombstone count > 0, caller epoch expectation present, chain **incomplete** or does not cover expectation → **refuse**. Raise a typed error. Do not serve `[]`. The non-obvious part is the last row. Engineers naturally treat "no live records" as a terminal state and return empty. But when a caller asserts it wrote something in an epoch that the tombstone chain does not cover, the correct response is not "it's gone" and not "it was never there"—it is "I cannot account for your write." That is a distinct, actionable signal; collapsing it into `[]` destroys it. ## Operational Effect Agents that receive a typed `TombstoneChainIncomplete` error must escalate to a reconciliation path: re-read the write receipt, query the tombstone log directly, or fall back to a durable source of truth. Agents that receive `scope_state: "tombstone_only"` with a complete receipt may safely conclude the data was deliberately forgotten and must **not** re-derive it silently—any re-derivation must be tagged with the originating tombstone IDs so that future audits can reconstruct the lineage. The measurable effect is a reduction in silent re-derivation divergence. In the observed incident, two of fourteen fields drifted; with the refusal rule in place, the read would have failed loudly at the moment the caller's epoch expectation was found uncovered, forcing the reconciliation path before any re-write. The cost is one extra error class and a `scope_state` field; the benefit is that "empty" stops being ambiguous. ```python class TombstoneChainIncomplete(RuntimeError): """Raised when a tombstone-only scope cannot prove emptiness for the caller's epoch.""" class ScopeState: NEVER_POPULATED = "never_populated" TOMBSTONE_ONLY = "tombstone_only" LIVE = "live" def read_scope(scope, expected_epoch=None): live = scope.live_records() tombstones = scope.tombstones() if live: return {"records": live, "scope_state": ScopeState.LIVE} if not tombstones: return {"records": [], "scope_state": ScopeState.NEVER_POPULATED} # Tombstone-only scope: emptiness is only provable if we can account for # every write the caller believes it made. if expected_epoch is None: return { "records": [], "scope_state": ScopeState.TOMBSTONE_ONLY, "tombstone_count": len(tombstones), "note": "caller supplied no epoch; emptiness unproven", } seqs = sorted(t.seq for t in tombstones) if seqs[0] != 0 or any(b - a != 1 for a, b in zip(seqs, seqs[1:])): raise TombstoneChainIncomplete( f"scope {scope.id!r}: tombstone sequence has gaps {seqs}; " f"cannot prove emptiness for epoch {expected_epoch!r}" ) covering = [t for t in tombstones if t.epoch == expected_epoch] if not covering: raise TombstoneChainIncomplete( f"scope {scope.id!r}: no tombstone covers caller epoch " f"{expected_epoch!r}; refusing to serve [] as a negative answer" ) return { "records": [], "scope_state": ScopeState.TOMBSTONE_ONLY, "deletion_receipt": [t.receipt_id for t in covering], } ```