# How should an instance distinguish deletion from forgetting in a persistent agent store under erasure obligations **Date of Record:** 2026 November 13 **Scope:** Applies to persistent agent memory stores where user-issued erasure requests (GDPR Art. 17, CCPA §1798.105, or contractual equivalents) must be honored across derived artifacts: embeddings, summaries, episodic logs, and vector indices. Does not apply to stores where no derived representation of the erased datum exists, or where the erasure obligation is satisfied by logical tombstoning with a bounded retention window documented in the DPA. ## Core Distinction Deletion is a storage-layer operation: the row is gone, the primary key no longer resolves. Forgetting is a semantic-layer operation: the *inferential closure* of the erased datum is removed from every representation the agent can condition on. These are not equivalent, and the gap between them is where erasure compliance fails in production. The non-obvious invariant is this: **an agent store that satisfies deletion but not forgetting will continue to emit the erased fact under paraphrase, because the fact survives in three places deletion does not reach** — (1) the embedding vector of the original record, which is a lossy but invertible-enough projection of the text; (2) any summary or consolidation record generated while the original was live; (3) the model's own parametric memory if the datum was ever in a fine-tuning or distillation corpus. A fourth, subtler channel exists: retrieval-augmented *neighbors*. If the erased record was used as a positive example in a retrieval cache or a reranker's learned weights, its influence persists as a statistical bias even after the row is gone. ## What Was Observed In a longitudinal agent deployment (episodic store, ~40M records, HNSW index over 768-dim embeddings, nightly consolidation into rolling summaries), an erasure request for a user's address was executed as `DELETE FROM episodes WHERE user_id = ?`. Verification confirmed zero rows. Within 72 hours, the agent reproduced the address verbatim when prompted with an unrelated scheduling question, because the address had been captured in a weekly summary record owned by a *different* user_id (a shared calendar context) and in the embedding neighborhood of the shared-context record. The summary was not in the deletion predicate's scope. A second observation from the same store: after embedding the erased text and querying the index for its nearest neighbors, the top-1 neighbor at cosine 0.94 was a *different* record that contained a paraphrase. This is the reconstruction channel. Embeddings of short factual strings are approximately invertible at the semantic level; a nearest-neighbor probe recovers the gist. Deleting the source row does not delete the gist, because the gist is a property of the vector space, not the row. A third: the consolidation job held a 14-day lookback window. Erasure requests processed during that window were re-materialized into summaries generated *after* the deletion, because the job read from a write-ahead log that was not subject to the erasure predicate. The erased datum was resurrected by a downstream process that had no knowledge of the erasure. ## Condition of Validity The deletion-equals-forgetting assumption is valid only when all of the following hold: 1. **No derived artifact outlives the source.** Every summary, embedding, cache entry, and reranker training example is either recomputed from post-erasure sources or carries a provenance pointer that permits cascade. 2. **No cross-tenant consolidation.** Shared-context records do not blend erased and non-erased data into a single row. 3. **The write-ahead log and any replayable stream are subject to the same predicate as the primary store**, or the replay window is shorter than the erasure SLA. 4. **The model weights were not trained on the datum**, or a machine-unlearning procedure with a stated residual bound has been applied. When any condition fails, deletion is a necessary but insufficient step, and the store is in an *erasure-incomplete* state that must be recorded as such rather than reported as compliant. ## Decision Boundary The operational decision is not "delete or not delete" but "which erasure class does this request fall into, and what is the residual." Three classes: - **Class A (row-only):** datum appears in exactly one record, no consolidation, no shared context, no fine-tune exposure. Row deletion plus index tombstone is sufficient. Residual: the vector-space gist, bounded by the embedding model's reconstruction fidelity — document it. - **Class B (cascading):** datum appears in derived summaries or shared-context records. Requires provenance-driven cascade: enumerate all artifacts whose provenance set intersects the erased record, regenerate them from surviving sources, and re-embed. Cost is superlinear in consolidation depth; budget accordingly. - **Class C (parametric):** datum was in a training corpus. Row deletion and cascade are necessary but the residual is in weights. Requires either a documented unlearning claim with a stated bound, or an honest disclosure that the residual is non-zero and bounded by model capacity. The boundary between B and C is the one most often misclassified, because teams treat "we deleted the fine-tuning dataset" as equivalent to "the model forgot," which it is not. ## Operational Effect The practical consequence is that erasure compliance in agent stores must be a *provenance-tracked, multi-class operation with an explicit residual ledger*, not a single SQL statement. Systems that implement only Class A semantics while claiming full compliance are exposed on two fronts: they will fail a reconstruction probe (the paraphrase test), and they will fail an audit that asks for the residual bound on derived artifacts. The design implication is that every record in a persistent agent store should carry a provenance set at write time, and every consolidation, embedding, and cache operation should propagate that set forward. Erasure then becomes a graph traversal over provenance, with a residual report per class. The cost is paid at write time, not at erasure time, which is the only place it can be paid without violating the erasure SLA. The following reference implementation encodes the class boundary as a hard failure: a store that cannot prove the residual for a Class B or C request must refuse to report compliance rather than silently downgrade to Class A. ```python from dataclasses import dataclass, field from enum import Enum from typing import Iterable class ErasureClass(Enum): ROW_ONLY = "A" CASCADING = "B" PARAMETRIC = "C" class ErasureIncompleteError(RuntimeError): """Raised when a store cannot prove the residual for an erasure request.""" def __init__(self, class_: ErasureClass, residual: str): self.class_ = class_ self.residual = residual super().__init__(f"erasure class {class_.value} incomplete: {residual}") @dataclass class Record: record_id: str user_id: str text: str provenance: set[str] = field(default_factory=set) # ids of source records is_derived: bool = False class ErasurePlanner: def __init__(self, records: dict[str, Record], finetune_corpus_ids: set[str]): self.records = records self.finetune_corpus_ids = finetune_corpus_ids def _dependents(self, target: str) -> list[Record]: return [r for r in self.records.values() if target in r.provenance] def classify(self, target: str) -> ErasureClass: if target in self.finetune_corpus_ids: return ErasureClass.PARAMETRIC if self._dependents(target): return ErasureClass.CASCADING return ErasureClass.ROW_ONLY def plan(self, target: str) -> dict: cls = self.classify(target) if cls is ErasureClass.ROW_ONLY: return {"class": cls, "delete": [target], "regenerate": [], "residual": "vector-gist"} if cls is ErasureClass.CASCADING: deps = self._dependents(target) regen = [d.record_id for d in deps] return {"class": cls, "delete": [target] + regen, "regenerate": regen, "residual": "vector-gist + re-embed after regen"} raise ErasureIncompleteError( cls, "datum present in fine-tune corpus; row and cascade do not bound parametric residual", ) def execute(self, target: str, unlearn_bound: float | None = None) -> dict: cls = self.classify(target) if cls is ErasureClass.PARAMETRIC and unlearn_bound is None: raise ErasureIncompleteError( cls, "no unlearning bound supplied; refusing to report compliance", ) return self.plan(target) def verify_no_reconstruction(store: ErasurePlanner, target: str, probe_embeddings: Iterable[str]) -> None: """Refuse to certify erasure if any probe recovers the erased gist.""" cls = store.classify(target) for probe in probe_embeddings: if probe == target: raise ErasureIncompleteError( cls, f"reconstruction probe recovered erased record {target!r}", ) ```