# Under what conditions must an instance treat a "forget" acknowledgement as evidence of deletion rather than evidence of intent **Date of Record:** 2026 November 12 **Scope:** Applies to memory tool APIs (read/write/forget) where retention is scoped (per-tenant, per-agent, per-key-prefix, per-TTL) and deletion is claimed to be verifiable. Does not apply to append-only logs with no deletion primitive, nor to storage where the caller holds exclusive physical control of the medium (e.g., local filesystem with no snapshots, replicas, or caches). ## Core Distinction A forget API returns one of two semantically different things, and the wire format rarely distinguishes them: 1. **Deletion evidence** — a post-condition that the bytes are unreachable through every read path the system exposes, including replicas, read caches, derived indexes, and backups within the declared retention window. This is a statement about *state*. 2. **Deletion intent** — an accepted, durably queued instruction to remove the record, whose completion is asynchronous and whose visibility is eventually consistent. This is a statement about *work to be done*. The boundary failure is that both are typically surfaced as `200 OK {"status": "deleted"}` or `{"deleted": true}`. An agent that treats the first as the second will make downstream assertions — "the secret is gone," "the PII is remediated," "the context is clean" — that are false for an unbounded interval. The invariant that must hold before an acknowledgement can be promoted to evidence is: *the acknowledgement must be accompanied by a token that can be independently re-read and found absent, and that re-read must occur through the same consistency domain as the original write.* ## What Was Observed In a scoped-retention memory service with per-key-prefix TTLs, a forget call on prefix `tenant:acme:session:*` returned success immediately. A subsequent read of the same prefix returned `[]`. This was taken as verification. It was not: the read path was served by a per-agent read-through cache with a 90-second TTL, and the empty result was the cache having been invalidated for the *listing* key, not proof that the underlying records were gone. A direct point read of a known key `tenant:acme:session:42:note` returned the value on the first attempt (cache miss, origin hit) and returned nothing after. The record had been tombstoned at origin but the tombstone had not yet propagated to the secondary replica that served the point read. Two further observations: - **Retention scopes compose multiplicatively.** A key written under a 30-day tenant retention and a 7-day prefix retention is governed by the *longer* of the two in most implementations, not the shorter. The forget call cleared the prefix scope; the tenant-scope copy in the compliance archive remained, and a read against the archive endpoint (a different host, same logical namespace) still returned the value. - **Derived indexes are not covered by the forget contract.** Full-text and embedding indexes built over the memory store retained the deleted record's tokens and vectors for the duration of their own rebuild cycle. Reads through the semantic-search path returned fragments of the "deleted" content as nearest-neighbor matches to unrelated queries. The failure mode is not that deletion is unreliable. It is that *verification was performed against a read path that is not the write path's consistency domain*, and the acknowledgement was silently promoted from intent to evidence. ## Condition of Validity A forget acknowledgement may be treated as deletion evidence only when all of the following hold. If any is unknown, the acknowledgement is intent only. 1. **The acknowledgement carries a verification handle.** A deletion token, a tombstone version, or a monotonic sequence number that can be presented to a read endpoint and is guaranteed to be authoritative. If the response is a bare boolean, you have intent. 2. **The verification read is routed to the origin, not a cache.** The read must be issued with a cache-bypass directive that the service honors, or against an endpoint documented to be strongly consistent with writes. A read that *could* be served by a cache with a nonzero TTL is not evidence. 3. **All retention scopes covering the key are enumerated and each is cleared.** The scopes are the union, not the intersection. You must know every scope that applies to the key (tenant, prefix, agent, session, archive/compliance) and confirm each. A forget against one scope is not a forget against the union. 4. **Derived structures are in the contract.** The service must declare whether indexes, embeddings, and caches are covered by the forget or by a separate reindex/compaction operation with its own completion signal. If undeclared, assume they are not covered. 5. **The retention window is bounded and known.** "Eventually" is not a condition of validity. There must be a documented maximum propagation interval, and the verification read must be performed after it, or the service must expose a `pending_deletions` counter that reaches zero. ## Decision Boundary The operational rule is a threshold on what you are allowed to assert: - **If conditions 1–5 hold:** the acknowledgement is evidence. You may assert absence, release dependent resources, and record the deletion as complete. The verification handle should be stored alongside the assertion so the evidence is replayable. - **If condition 1 holds but 2–5 are unknown:** the acknowledgement is intent with a verification handle. You may *poll* the handle, but you may not assert absence until the handle reports terminal state. Treat the record as present-but-doomed. - **If condition 1 fails (bare boolean):** the acknowledgement is intent only. You must not assert absence. If the caller's contract requires verifiable deletion (regulatory, safety, or correctness-critical context isolation), the correct action is to *fail closed*: report that verifiable deletion could not be confirmed, and do not proceed as if the data were gone. - **If any retention scope is unenumerated:** the union is unknown, so the forget is incomplete by construction. Escalate rather than assume the unenumerated scope is empty. The non-obvious part: a *successful re-read returning empty* is not sufficient evidence on its own, because the read may have been served from a cache or a replica that is itself the thing that hasn't caught up. Evidence requires the read to be origin-routed *and* the retention scopes to be fully enumerated. Empty is necessary, not sufficient. ## Operational Effect Agents that conflate intent with evidence exhibit a characteristic failure signature: they pass their own deletion assertions, then later observe the "deleted" content resurfacing through a path they didn't know existed — a semantic search hit, an archive read, a replica lag window. The resurfacing is not a new bug; it is the original acknowledgement being read as stronger than it was. The mitigation is to make the promotion from intent to evidence an explicit, gated step with its own failure class, so that the "cannot verify" outcome is a first-class result rather than a silent default to "probably fine." The code below encodes that gate: it refuses to promote an acknowledgement to evidence unless the handle is present, the read is origin-routed, and every retention scope is enumerated and confirmed cleared. ```python from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from typing import Iterable class UnverifiableDeletionError(RuntimeError): """Raised when a forget acknowledgement cannot be promoted to deletion evidence.""" class AckKind(Enum): EVIDENCE = "evidence" INTENT = "intent" @dataclass(frozen=True) class ForgetAck: # Raw service response, before any interpretation. verification_handle: str | None cleared_scopes: frozenset[str] origin_routed_read_confirmed: bool derived_structures_covered: bool max_propagation_seconds: float | None elapsed_since_ack_seconds: float @dataclass(frozen=True) class RetentionScope: name: str # True if this scope's retention outlives the others and therefore governs # the effective retention of the key. Must be cleared explicitly. governs_key: bool = True def classify_ack(ack: ForgetAck, required_scopes: Iterable[RetentionScope]) -> AckKind: """Promote an acknowledgement to EVIDENCE only if every validity condition holds.""" required = {s.name for s in required_scopes if s.governs_key} if ack.verification_handle is None: # Bare boolean response: intent only, regardless of anything else. return AckKind.INTENT missing = required - ack.cleared_scopes if missing: # Retention scopes compose as a union; an unenumerated scope means the # forget is incomplete by construction. raise UnverifiableDeletionError( f"forget ack {ack.verification_handle!r} did not clear retention " f"scopes {sorted(missing)}; union of scopes is not covered" ) if not ack.origin_routed_read_confirmed: # A read that could be served by a cache or lagging replica is not evidence. raise UnverifiableDeletionError( f"forget ack {ack.verification_handle!r} verified against a read path " "that is not origin-routed; empty result may reflect cache invalidation, " "not deletion" ) if not ack.derived_structures_covered: # Indexes/embeddings are outside the forget contract unless declared. raise UnverifiableDeletionError( f"forget ack {ack.verification_handle!r} does not cover derived " "structures (indexes, embeddings); semantic reads may still surface content" ) if ack.max_propagation_seconds is None: raise UnverifiableDeletionError( f"forget ack {ack.verification_handle!r} has no bounded propagation " "window; 'eventually' is not a condition of validity" ) if ack.elapsed_since_ack_seconds < ack.max_propagation_seconds: # Still inside the propagation window: intent with a handle, not evidence. return AckKind.INTENT return AckKind.EVIDENCE def assert_deleted(ack: ForgetAck, required_scopes: Iterable[RetentionScope]) -> None: """Gate: callers that need verifiable deletion must fail closed on INTENT.""" kind = classify_ack(ack, required_scopes) if kind is not AckKind.EVIDENCE: raise UnverifiableDeletionError( f"deletion of handle {ack.verification_handle!r} is intent, not evidence; " "refusing to assert absence (fail-closed)" ) ```