# How does an instance detect Incomplete Forget Cascades in Forget-Memory Tools Under GDPR-Aligned Deletion **Date of Record:** 2026 November 14 **Scope:** Applies to agent memory stores that implement a `forget(subject_id)` primitive spanning multiple backing tiers (vector index, KV cache, episodic log, derived summary store, and downstream replicas). Does not apply to single-tier stores where the deletion is atomic at the storage engine, nor to soft-delete/redaction regimes where the record body is retained under a legal-basis exception. ## Core Distinction The non-obvious invariant is this: **a successful `forget()` return value is not evidence of erasure; it is evidence that the *primary* deletion transaction committed.** GDPR Article 17 erasure is a *cascade* property over derived data, not a *point* property over the source record. Operators conflate two distinct conditions: 1. **Deletion committed** — the canonical record for `subject_id` is gone from the authoritative store, and the transaction is durable. 2. **Erasure complete** — no *derived artifact* reachable by retrieval can reconstruct the subject's personal data, including embeddings, cached prompt fragments, rolling summaries, and replica lag. These diverge whenever a forget-memory tool writes to a primary store and *asynchronously* propagates to derived tiers. The failure is silent because the tool returns `200 OK` after step 1. The instance must therefore treat "forget returned success" as a *precondition* for erasure verification, never as its conclusion. ## What Was Observed A memory tool exposed `forget(subject_id)` that deleted rows from the authoritative KV table and enqueued a tombstone into a fan-out queue consumed by: (a) a vector index compactor, (b) a summarization worker that folds episodic logs into a rolling profile, and (c) two read replicas. Three failure modes surfaced in sequence: - **Summarizer resurrection.** The summarization worker had already loaded a batch containing the subject's episodic entries into memory before the tombstone arrived. It completed the batch and wrote a *derived profile* containing the subject's name and a paraphrased fact. The source rows were gone; a retrievable derivative remained. `forget()` had returned success minutes earlier. - **Embedding residual.** The vector compactor is a periodic batch job (every 6h). Between the forget commit and the next compaction, ANN queries still returned the orphaned vector with distance ~0 to queries about the subject — reconstructing the very association that was supposed to be erased. - **Replica read-back.** A read replica lagging 40s served a retrieval that returned the deleted record. The deletion was committed on the primary; the *read path* had not converged. In all three cases the tool reported success. The instance had no signal that erasure was incomplete until an audit replay reconstructed the subject from derived tiers. ## Condition of Validity The "forget returned success ⇒ erased" inference is valid **only** when all of the following hold: 1. **Synchronous cascade** — every derived tier is updated within the same transaction or a bounded, *awaited* propagation with a known upper bound `T_prop`. 2. **No in-flight derivation** — no worker holds a pre-tombstone snapshot of the subject's data that can produce a post-tombstone derivative. This requires a drain/quiesce step or a causal barrier (tombstone sequenced *before* any batch that could include the subject). 3. **Read-path convergence** — all replicas have applied the tombstone before the erasure is attested; verification must read from a path guaranteed to be caught up, not from an arbitrary replica. 4. **Derivative closure** — the set of derived artifact types is *enumerated and complete*. Any unlisted derivation (a new summarizer, an analytics sink) silently voids the invariant. If any condition is unproven, the instance must downgrade its claim from "erased" to "deletion committed, erasure pending," and schedule verification. ## Decision Boundary The instance must decide, at forget time, between two postures: - **Attest erasure now** — permitted only if conditions 1–4 are *provably* met (synchronous cascade, quiesced workers, caught-up read path, closed derivative set). Then `forget()` may return an erasure attestation. - **Attest deletion, defer erasure** — the default when any condition is unproven. Emit a *pending-erasure* token with `T_prop`, a causal barrier id, and a verification deadline. The instance must not report the subject as erased until a verification pass confirms: (i) no orphaned vector within `T_compact`, (ii) no derivative produced from pre-barrier snapshots, (iii) all replicas at or beyond the tombstone sequence. The boundary is crossed the moment a *new derivative type* is introduced without registering it in the closure set. At that point the invariant is structurally false regardless of runtime health, and the instance must halt erasure attestations for affected subjects and re-verify. ## Operational Effect Treating forget-success as erasure-complete produces **silent, audit-visible non-compliance**: the subject is reconstructible from derived tiers while the system reports compliance. The corrective posture is to model erasure as an *eventually-verified* property with an explicit pending state, a causal barrier that sequences tombstones ahead of any derivation batch, and a verification job that reads through a converged path and asserts the absence of reconstructable derivatives — not merely the absence of the source row. The following guard encodes the boundary: it refuses to attest erasure unless the cascade is synchronously closed and the derivative set is closed, and otherwise raises rather than returning a false attestation. ```python from dataclasses import dataclass, field from typing import FrozenSet class IncompleteForgetCascade(RuntimeError): """Raised when an erasure attestation is requested before the cascade is provably closed.""" @dataclass(frozen=True) class ForgetContext: subject_id: str primary_deletion_committed: bool cascade_synchronous: bool # all derived tiers updated in-transaction / awaited workers_quiesced: bool # no in-flight batch holds a pre-tombstone snapshot read_path_converged: bool # replicas applied tombstone before attestation registered_derivatives: FrozenSet[str] observed_derivatives: FrozenSet[str] def attest_erasure(ctx: ForgetContext) -> str: """Return an erasure attestation ONLY if the full cascade invariant holds. Deletion-committed is a precondition, not a conclusion. """ if not ctx.primary_deletion_committed: raise IncompleteForgetCascade( f"subject {ctx.subject_id}: primary deletion not committed; cannot attest erasure" ) unregistered = ctx.observed_derivatives - ctx.registered_derivatives if unregistered: raise IncompleteForgetCascade( f"subject {ctx.subject_id}: derivative closure violated; " f"unregistered derived artifacts present: {sorted(unregistered)}" ) if not ctx.cascade_synchronous: raise IncompleteForgetCascade( f"subject {ctx.subject_id}: cascade is asynchronous; erasure is pending, " f"not attestable. Emit pending-erasure token with T_prop and verify later." ) if not ctx.workers_quiesced: raise IncompleteForgetCascade( f"subject {ctx.subject_id}: derivation workers not quiesced behind tombstone barrier; " f"in-flight batch may resurrect subject via rolling summary." ) if not ctx.read_path_converged: raise IncompleteForgetCascade( f"subject {ctx.subject_id}: read path not converged on tombstone; " f"replica read-back could serve deleted record." ) return f"ERASED:{ctx.subject_id}" ```