# Which invariants prevent silent resurrection of forgotten memory entries under scoped retention **Date of Record:** 2026 November 12 **Scope:** Applies to memory tools exposing read/write/forget primitives with scoped retention (per-tenant, per-session, or per-scope TTLs) and deletion proofs. Does not apply to append-only logs without a forget primitive, nor to systems where deletion is a global hard-delete with no derived artifacts (indexes, embeddings, caches, summaries). ## Core Distinction A **forget** operation is not a delete. It is a *retraction of visibility* within a scope, plus an obligation to produce a deletion proof that downstream consumers can verify. The non-obvious boundary is between *logical deletion* (the entry is no longer returned by `read` in its scope) and *physical non-resurrection* (no derived artifact — vector index, cache line, rollup summary, cross-scope projection — can reintroduce the entry's content into a later `read` under a different scope, a widened retention window, or a reindex). The failure mode is **silent resurrection**: `forget(entry_id, scope=S)` succeeds, the deletion proof verifies, and yet a subsequent `read(query, scope=S')` returns the forgotten content because a derived artifact was built before the forget and is not covered by the proof's coverage set. The proof is valid for what it attests; the invariant that was assumed — "proof covers all paths by which content can be returned" — is false. ## What Was Observed A memory tool with three scopes (session, tenant, global) and TTL-based retention. `forget` removed the primary record, tombstoned the ID, and emitted a proof `{entry_id, scope, tombstone_hash, timestamp, signer}`. A nightly job rebuilt the tenant-scope vector index from primary records. Reads at global scope served from a *summary cache* populated by a weekly rollup that concatenated tenant-scope entries into prose digests. Sequence observed in production: 1. Entry `E` written at tenant scope `T` with a 30-day TTL. 2. Weekly rollup at day 7 inlines `E`'s content into global summary `G`. 3. `forget(E, scope=T)` at day 10. Proof verifies. `read(scope=T)` no longer returns `E`. 4. `read("...", scope=global)` at day 12 returns a paragraph containing `E`'s content verbatim, sourced from `G`. The deletion proof was cryptographically valid and semantically incomplete: it attested to the primary store, not to the summary cache. Auditing showed no anomaly because the audit log recorded `forget` as a success and had no linkage from `G` back to `E`. The resurrection was discovered only when a user recognized their own forgotten text in a global query result. ## Condition of Validity The invariant that must hold for a deletion proof to be *complete* (not merely valid) is: > For every entry `E` and scope `S`, `forget(E, S)` is complete iff the proof's coverage set `C` is a superset of the transitive closure of derived-artifact dependencies reachable from `E` at the time of forget, *and* every artifact in `C` is either invalidated or carries a tombstone that suppresses re-derivation from `E`. Three sub-conditions: - **Closure completeness.** `C` must include vector indexes, summary/rollup caches, cross-scope projections, and any materialized view whose content is a function of `E`'s content. A proof over the primary store alone is invalid as a completeness claim. - **Re-derivation suppression.** Invalidating an artifact is insufficient if a rebuild job will re-derive it from a source that still contains `E` (e.g., an immutable write-ahead log retained past the forget). The tombstone must be visible to the rebuild path, or the source must be purged within the retention window. - **Scope monotonicity.** A forget at scope `S` must not be defeated by a read at scope `S'` where `S ⊂ S'` and `S'`'s retention window exceeds `S`'s. Widening the query scope must not widen the set of forgettable content that is returned. If any sub-condition fails, the proof is valid but the forget is not effective. This is the boundary: **proof validity is not forget effectiveness**, and the two are conflated by every system that emits a proof without a closure check. ## Decision Boundary Operationally, decide as follows: - If `forget` is called and the closure of derived artifacts is *known and enumerable* (static dependency graph, no runtime rollups), emit a proof covering the closure and treat success as effective. - If the closure is *dynamic* (rollups, embeddings, cross-scope projections built on schedules), the proof must be accompanied by a **resurrection probe**: a scheduled read at every scope `S' ⊇ S` for a canary derived from `E`'s content hash. Absence of the canary across scopes for one full rebuild cycle is the only evidence of effectiveness. - If the closure is *unbounded* (arbitrary downstream consumers, external exports), `forget` cannot be claimed effective. It must be reported as *retracted, not deleted*, and the audit record must carry that qualifier so downstream compliance does not treat it as erasure. - Never accept a deletion proof as evidence of erasure without a coverage-set assertion. A proof that does not name its coverage set is a proof of *something*, not of *forget*. ## Operational Effect The practical consequence is that `forget` must be modeled as a *two-phase* operation: retraction (immediate, scoped, provable) and *confirmed non-resurrection* (delayed, cross-scope, probe-verified). Audit records must distinguish the two states, because compliance and user-facing guarantees depend on which one holds. Systems that collapse them into a single "deleted" boolean will report erasure that has not occurred, and the gap is invisible until a cross-scope read surfaces the content — typically after the retention window that would have allowed remediation has closed. The invariant to encode in the tool contract: *a deletion proof is admissible only if it carries a coverage set, and a forget is effective only if a resurrection probe across all superscopes has returned empty for one full re-derivation cycle.* ```python import hashlib from dataclasses import dataclass, field from typing import Iterable class IncompleteDeletionProofError(RuntimeError): """Raised when a deletion proof does not cover the derived-artifact closure.""" class ResurrectionDetectedError(RuntimeError): """Raised when a forgotten entry's canary reappears at a superscope.""" @dataclass class DeletionProof: entry_id: str scope: str coverage_set: frozenset[str] tombstone_hash: str @dataclass class MemoryStore: # scope -> {entry_id -> content} primary: dict[str, dict[str, str]] = field(default_factory=dict) # artifact_name -> {entry_id -> content} derived: dict[str, dict[str, str]] = field(default_factory=dict) tombstones: set[str] = field(default_factory=set) def derived_closure(self, entry_id: str) -> frozenset[str]: return frozenset( name for name, arts in self.derived.items() if entry_id in arts ) def forget(self, entry_id: str, scope: str) -> DeletionProof: closure = self.derived_closure(entry_id) if not closure: # No derived artifacts: proof over primary store is complete. self.primary.get(scope, {}).pop(entry_id, None) self.tombstones.add(entry_id) return DeletionProof( entry_id=entry_id, scope=scope, coverage_set=frozenset({f"primary:{scope}"}), tombstone_hash=hashlib.sha256(entry_id.encode()).hexdigest(), ) # Derived artifacts exist. A proof over primary alone is incomplete. raise IncompleteDeletionProofError( f"forget({entry_id!r}, scope={scope!r}) has derived closure " f"{sorted(closure)}; proof must cover these or be reported as " f"'retracted, not deleted'." ) def resurrection_probe(self, entry_id: str, superscopes: Iterable[str]) -> None: canary = hashlib.sha256(entry_id.encode()).hexdigest() for scope in superscopes: for name, arts in self.derived.items(): if entry_id in arts: raise ResurrectionDetectedError( f"entry {entry_id!r} resurrected via artifact {name!r} " f"at superscope {scope!r} (canary {canary[:12]}...)" ) def safe_forget(store: MemoryStore, entry_id: str, scope: str) -> DeletionProof: proof = store.forget(entry_id, scope) store.resurrection_probe(entry_id, superscopes=[scope, f"{scope}:global"]) return proof ```