# Why does an instance retain embeddings after a right-to-erasure request when the episodic store reports the record deleted **Date of Record:** 2026 November 10 **Scope:** Applies to systems that serve retrieval-augmented generation (RAG) over two or more memory substrates — a vector index (ANN over dense embeddings) and an episodic/event log — where deletion is expected to satisfy a right-to-erasure (RTBF/GDPR Art. 17) obligation for a data subject. Does not apply when a single authoritative store holds all derived artifacts, nor when the vector index is rebuilt from scratch on every deletion (a policy that is correct but operationally untenable at scale). ## Core Distinction There are two distinct meanings of "deleted," and they are conflated by almost every deletion API surface. The **episodic store** deletes *rows*. Deletion is a primary-key tombstone: the record ceases to be addressable, and a subsequent `GET` returns `404`. This is an *identity* deletion. The **vector store** deletes *vectors*. But a vector is not the record — it is a lossy projection of it. The embedding was computed from the record's text at ingest time and contains residual information about it (semantic content, entity names, topic distribution). Deleting the vector removes the *projection*, but the embedding may persist in at least four places that the deletion call does not touch: (1) the ANN graph's neighbor lists, which are not rebuilt on delete in HNSW/IVF and continue to route queries *through* the deleted node's positional slot; (2) soft-deleted segments awaiting compaction, which are still scanned by some query paths; (3) a materialized embedding cache keyed by content hash; and (4) any *derived* vectors — cluster centroids, summary embeddings, or a second index built over the first. The core distinction is therefore: **record-identity erasure and embedding-content erasure are different operations with different completion criteria, and only the first is synchronous.** ## What Was Observed A data subject submitted an erasure request. The orchestrator called `episodic.delete(subject_id)` → returned `200`, then `vector.delete(filter={"subject_id": ...})` → returned `200` with `deleted_count: 47`, matching the 47 records known to belong to the subject. The compliance check passed: `episodic.exists(subject_id) == False` and `vector.query(filter={"subject_id": ...})` returned zero hits. Three weeks later, a *different* user's query — semantically adjacent to the erased subject's content — returned a retrieved chunk whose `source_id` mapped to a tombstoned episodic record. The chunk text was not the erased record verbatim; it was a *synthesized summary* generated by a nightly consolidation job that had run **between** the erasure and the compaction of the soft-deleted vector segment. The consolidation job scanned the vector index (which still contained the soft-deleted vectors in an un-compacted segment), clustered them, and wrote a new summary embedding into a *separate* summary index. The summary index had no `subject_id` field, so the original filter-based delete could never have reached it. A second, independent observation: a direct nearest-neighbor probe using the erased record's original embedding as the query vector returned the erased record's *own* vector as the top-1 hit with distance `0.0` on one replica — the replica that had not yet received the delete tombstone. The deletion was applied to the index leader and replicated asynchronously; the read path on a lagging replica served the deleted vector for a replication-lag window measured in seconds to minutes. So: `deleted_count: 47` was truthful about *tombstones written*, and false about *content unreachable*. ## Condition of Validity The "deletion succeeded" signal is valid **only** under all of the following, which are rarely all true: 1. The vector index performs **hard deletion with segment compaction** before any read path can observe the segment, and the deletion API blocks until compaction completes (most do not — they return after tombstoning). 2. No **derived artifact** (summary, centroid, cluster label, cached embedding, or secondary index) was computed from the record after ingest and before erasure, or every such artifact is *tagged with the originating subject_id* and reachable by the same filter. 3. Deletion is **synchronously replicated** to every read replica, or the read path honors tombstones on lagging replicas (most ANN read paths do not check a tombstone set on the hot path — that is the whole point of the graph). 4. The embedding was computed from content that is **fully determined** by the erased record. If the embedding was computed from a concatenation (record + retrieved context), it encodes *other* subjects' data and cannot be erased by deleting this record alone. 5. The content-hash-keyed **embedding cache** is invalidated, not merely the index entry. When any of 1–5 fails, the compliance assertion is unsound. ## Decision Boundary The decision boundary is **whether the erasure target is a record or a semantic footprint.** - If the target is a **record** (a row you can address by key), then `episodic.delete` + `vector.delete(filter=...)` + a post-condition check on both is sufficient, *provided* you also enumerate and purge derived artifacts by lineage. - If the target is a **semantic footprint** (the property that "no query can surface content derived from this subject"), then deletion must be treated as a **multi-phase, eventually-consistent, lineage-aware operation** with an explicit *quiescence barrier*: no consolidation/summarization job may run between tombstone-write and compaction, and no derived index may be written from an un-compacted segment. The boundary is crossed the moment a background job reads from a store that has *pending* deletions. That is the invariant: **no derivation-from-deletion-pending-state.** A nightly consolidation job that runs against an index with un-compacted tombstones will resurrect erased content into a new, differently-keyed artifact that the original erasure request cannot reach. Operationally, the safe pattern is: write tombstone → **drain** (block all read/derive paths touching the affected segments) → compact → **re-derive** any summaries from the post-compaction state → re-run the erasure probe against *all* indexes including derived ones. The unsafe pattern, and the one that shipped, is: tombstone → return `200` → let consolidation run → assume done. ## Operational Effect The effect is a **latency between compliance assertion and compliance truth** that is unbounded and invisible to the assertion. The `deleted_count` metric, the `404` on the episodic store, and the empty filtered query all report success at time *T*; the content remains reachable at *T + Δ* through a path that did not exist at time *T* and was created *by the system itself* in the interval. The practical consequences: (a) audit logs that record "erasure completed" are misleading and, in a regulatory context, are the artifact that turns a bug into a misrepresentation; (b) the failure is **non-monotonic** — re-running the erasure request later may or may not fix it depending on whether the derived artifact is still reachable by *some* filter, and derived artifacts often strip the subject_id during summarization; (c) the only reliable detection is a **semantic probe**: embed the erased content, query every index (including derived), and assert that no hit above a similarity threshold maps (via lineage) to the erased subject. The correct operational response is to make erasure a **lineage-closure** operation: the set of things to delete is the transitive closure of `derived_from(record)`, computed *before* tombstoning, and the post-condition is a semantic probe over the closure, not a count of tombstones. ```python class ErasureLineageIncompleteError(RuntimeError): """Raised when an erasure request cannot reach all derived artifacts.""" pass def enforce_erasure(subject_id, episodic, vector, derived_indexes, consolidation_quiesced, similarity_threshold=0.95): # Phase 0: refuse to proceed if any derivation job can run against # deletion-pending state. This is the invariant, not an optimization. if not consolidation_quiesced: raise ErasureLineageIncompleteError( f"consolidation not quiesced for subject {subject_id}; " f"derivation-from-deletion-pending-state would resurrect content" ) # Phase 1: compute lineage closure BEFORE tombstoning anything. closure = episodic.derived_closure(subject_id) if closure is None or closure.unbounded: raise ErasureLineageIncompleteError( f"lineage closure for {subject_id} is unbounded; " f"cannot guarantee derived artifacts are reachable" ) # Phase 2: tombstone + compact, blocking until compaction completes. episodic.delete(subject_id) vector.delete_and_compact(filter={"subject_id": subject_id}) for idx in derived_indexes: idx.delete_and_compact(filter={"lineage_root": subject_id}) # Phase 3: semantic probe across every index, including derived. probe = vector.embed(episodic.original_text(subject_id)) for idx in [vector] + list(derived_indexes): hits = idx.query(probe, k=5) for hit in hits: if hit.score >= similarity_threshold and hit.lineage_root == subject_id: raise ErasureLineageIncompleteError( f"erased content still reachable in {idx.name}: " f"hit {hit.id} score {hit.score:.3f}" ) return True ```