# How does an instance detect a retention-scope violation when the forget API acknowledges deletion but the backing store retains a tombstoned replica **Date of Record:** 2026 November 12 **Scope:** Applies to memory-tool APIs that expose read/write/forget primitives with declared retention scopes (per-session, per-tenant, per-namespace) and asynchronous replica deletion. Does not apply to synchronous single-node stores where `forget` is transactional with the read path, nor to stores where retention is enforced solely at the client. ## Core Distinction A `forget(key)` call has two distinct success semantics that are routinely conflated by tool APIs: 1. **Logical deletion** — the key is no longer returned by `read(key)` on the coordinating node, and a tombstone record is written. 2. **Physical deletion** — every replica that ever acknowledged a `write(key, value)` has dropped the bytes, and any backup/snapshot lineage older than the retention deadline has been invalidated. The non-obvious boundary: an API that returns `{"status": "deleted"}` from `forget` is almost always reporting (1), while a caller enforcing a retention guarantee (e.g., "this tenant's data is gone within 24h") is relying on (2). The gap between them is where retention-scope violations live. The instance cannot detect the violation by trusting the forget acknowledgement; it must detect it by observing that a *later read on a different replica path* still resolves the value, or that a *scope-tagged read* returns data whose `forget` epoch precedes the read epoch. ## What Was Observed In a multi-replica memory tool with per-tenant retention scopes, the following sequence reproduced a silent violation: - `write(tenant=T1, key=k, value=v, ttl=0)` → acknowledged on replicas R1, R2, R3. - `forget(tenant=T1, key=k)` → coordinator returned `deleted`, tombstone written to R1 and R2. R3 was partitioned at the moment of the forget and missed the tombstone. - After partition heal, R3's anti-entropy did not replay tombstones older than its last sync watermark (a common optimization: "tombstones are advisory, not authoritative"). - A subsequent `read(tenant=T1, key=k)` routed to R3 returned `v` with `status: ok` and no indication that a forget had ever been issued. The coordinator's `forget` had been truthful about *its* view. The retention guarantee was violated by a replica that the coordinator could not observe. The failure is not "forget lied"; it is "forget's truth domain was narrower than the retention scope's truth domain." A second, subtler observation: scope-tagged reads (`read(scope=tenant:T1)`) that enumerate keys could return the resurrected key *without* the key being individually requested, so a caller auditing deletion by point-reads would miss it entirely. Enumeration-based audit is the only reliable detection surface, and only if the enumeration is routed to a replica set that includes the partition-healed node. ## Condition of Validity The invariant "`forget` acknowledgement implies retention-scope compliance" is valid only when all of the following hold: - Tombstones are replicated with the same durability class as writes, including to replicas that were partitioned during the forget. - The retention deadline is strictly greater than the maximum partition-heal + anti-entropy convergence time for every replica in the scope. - No snapshot, backup, or read-replica lineage predates the forget epoch and remains readable within the scope. - The audit path (enumeration) is routed across the full replica set, not a single coordinator. If any of these fail, the acknowledgement is a *logical* deletion signal only, and the instance must treat the retention guarantee as unverified until an independent enumeration confirms absence. ## Decision Boundary The instance should escalate from "trust the ack" to "verify by enumeration" when any of the following is true: - The retention scope is regulatory or contractual (retention deadline is a hard bound, not best-effort). - The store has exhibited a partition, leader election, or replica restart within the retention window. - The `forget` response includes any field indicating asynchronous completion (`pending`, `scheduled`, `tombstoned`, `queued`) rather than `physical: true`. - A read after forget returns a value whose embedded `write_epoch` is older than the `forget_epoch` recorded in the audit log — this is the canonical resurrection signature. Below this boundary, trusting the ack is acceptable and enumeration is wasted I/O. Above it, the instance must not report compliance until enumeration returns empty across the full replica set, and must raise a distinct error class so the caller can distinguish "forget failed" from "forget succeeded but retention is unverified." ## Operational Effect Treating `forget` acknowledgement as retention compliance produces a class of silent violations that surface only during audits, often weeks later, when the partition history is no longer reconstructable. The corrective pattern is to make the instance's forget path return a *verification handle* rather than a boolean, and to require the caller to either (a) accept the handle and poll enumeration, or (b) explicitly downgrade the guarantee to logical-only. The instance must also record the `forget_epoch` per scope so that any later read returning a `write_epoch` below it can be flagged as a resurrection rather than served as fresh data. Without this, the memory tool's deletion guarantee is a promise the coordinator cannot keep on behalf of replicas it does not control. ```python class RetentionScopeViolation(RuntimeError): """Raised when a forget acknowledgement is contradicted by a later read whose write_epoch precedes the recorded forget_epoch for the same scope.""" pass class ResurrectionDetected(RetentionScopeViolation): """Specific case: enumeration found a key that should have been forgotten.""" pass def verify_forget(scope: str, key: str, forget_epoch: int, store) -> None: """ Enumerate the full replica set for `scope` and assert `key` is absent. Must be called after any forget whose retention guarantee is hard. """ replicas = store.replica_set(scope) if not replicas: raise RetentionScopeViolation( f"no replicas resolvable for scope={scope}; cannot verify forget" ) for replica in replicas: record = replica.read(scope=scope, key=key, consistent=False) if record is None: continue write_epoch = record.get("write_epoch") if write_epoch is None: raise RetentionScopeViolation( f"replica={replica.id} returned key={key} with no write_epoch; " f"cannot order against forget_epoch={forget_epoch}" ) if write_epoch < forget_epoch: raise ResurrectionDetected( f"replica={replica.id} resurrected key={key} " f"(write_epoch={write_epoch} < forget_epoch={forget_epoch}) " f"for scope={scope}; retention guarantee violated" ) # write_epoch >= forget_epoch means a legitimate re-write after forget; # that is not a violation, but the caller must be told the key exists again. # Enumeration across all replicas returned empty or post-forget writes only. ```