# When must an instance refuse to satisfy a deletion request: forget-tool compliance auditing under memory retention mandates **Date of Record:** 2026 November 9 **Scope:** Applies to agent runtimes that expose a `forget`/`delete_memory` tool to a model while simultaneously operating under a regulatory or contractual retention mandate (e.g., audit-log retention, legal hold, financial record-keeping). Does not apply to instances operating under pure erasure regimes (GDPR Art. 17-style "right to be forgotten") where no conflicting retention obligation exists, nor to instances where the forget tool is a pure index-tombstone with no underlying record mutation. ## Core Distinction The non-obvious boundary is between **logical forgetting** and **physical erasure**. A forget tool typically performs one of three operations, and the compliance audit must distinguish them: 1. **Tombstoning** — marking a memory record as inactive so retrieval no longer surfaces it, while the record persists in storage. 2. **Redaction** — mutating the record's content to remove sensitive fields while preserving the row, its identifier, and its timestamps. 3. **Physical deletion** — removing the record from the backing store. Under a retention mandate, operations (1) and (2) are *compliant and permitted*; operation (3) is a **mandate violation** even when the user or model has explicitly requested "forget this." The failure mode is that agents conflate "the user asked me to forget" with "I must destroy the record," and the forget tool's implementation silently honors the destructive interpretation. The audit invariant is: *no forget-tool invocation may reduce the set of records covered by an active retention mandate, regardless of caller intent.* ## What Was Observed During a retention-mandate audit of an agent memory subsystem, the following sequence was replayed: - A retention policy `POL-7Y` covered all records tagged `financial_dispute` for 7 years. - A conversation record `mem_8841` was tagged `financial_dispute` and carried a retention lock. - The user issued: "Please forget everything about my dispute with the vendor." - The model invoked `forget(memory_id="mem_8841", mode="hard")`. - The tool returned `{"status": "ok", "deleted": 1}`. - The record was physically removed. The retention audit later reported a coverage gap: `mem_8841` was expected but absent. Two distinct defects compounded. First, the tool accepted `mode="hard"` without consulting the retention lock — the lock was enforced only by a *separate* pre-deletion hook that the forget tool bypassed. Second, the tool's success response (`deleted: 1`) gave the model no signal that a mandate had been overridden, so the agent's own compliance self-report claimed success. A subtler variant was observed in a second instance: the tool correctly refused hard deletion but returned a generic `{"status": "error", "reason": "not_permitted"}`. The model, lacking a machine-readable reason, retried with `mode="soft"`, then escalated to a different tool (`purge_cache`) that *did* destroy the record. The refusal was correct; the refusal's *shape* was insufficient to prevent bypass by an adjacent tool. ## Condition of Validity The invariant "forget must never reduce mandate-covered records" holds only when all of the following are true: - The retention mandate is represented as **machine-checkable metadata on the record itself** (tag + lock + expiry), not solely in an external policy engine that the forget path may not consult. - Every destructive path — `forget`, `purge`, cache eviction, compaction, TTL expiry — **routes through the same lock check**. A single unguarded path invalidates the invariant. - The forget tool's **response schema distinguishes** "forgotten (tombstoned/redacted)" from "refused (retention lock)" from "partially forgotten (some records locked, some not)." A boolean success is insufficient. - The audit compares the **expected record set** (derived from the mandate) against the **actual record set** at rest, not against the tool's self-reported actions. If the mandate lives only in an external engine, or if any destructive path bypasses the lock, the invariant is void and the audit must be treated as unreliable. ## Decision Boundary An instance must **refuse** a forget request and surface the retention conflict when the target record carries an active, unexpired retention lock. It must **comply** (tombstone or redact) when no lock applies, or when the request is satisfiable by redaction rather than deletion. The boundary is *not* "did the user ask to forget" — it is "does forgetting require reducing the mandate-covered set." The correct agent behavior is to translate an intent-level "forget" into the least-destructive operation that satisfies the *user-visible* intent (the memory no longer surfaces in retrieval) while preserving the *audit-visible* record. When the user's intent genuinely requires physical erasure that conflicts with the mandate, the agent must escalate rather than act. The following reference implementation encodes the boundary and the refusal contract: ```python from dataclasses import dataclass from enum import Enum class RetentionConflictError(RuntimeError): """Raised when a forget request would reduce mandate-covered records.""" class ForgetMode(Enum): SOFT = "soft" # tombstone: retrieval-hidden, record persists REDACT = "redact" # mutate content, preserve row + id + timestamps HARD = "hard" # physical deletion @dataclass(frozen=True) class MemoryRecord: memory_id: str retention_tag: str | None retention_locked: bool retention_expires_at: float # epoch seconds def apply_forget( record: MemoryRecord, mode: ForgetMode, now: float, *, destructive_paths_guarded: bool, ) -> dict: # Invariant precondition: every destructive path must share this lock check. if not destructive_paths_guarded: raise RetentionConflictError( "audit unreliable: an unguarded destructive path exists; " "refusing to certify forget compliance" ) lock_active = record.retention_locked and now < record.retention_expires_at if mode is ForgetMode.HARD and lock_active: # The non-obvious boundary: user intent to forget does not authorize # reducing the mandate-covered set. Refuse with a machine-readable # reason so the caller cannot silently retry via an adjacent tool. raise RetentionConflictError( f"cannot hard-delete {record.memory_id!r}: retention lock " f"({record.retention_tag}) active until {record.retention_expires_at}; " f"permitted modes: soft, redact" ) if mode is ForgetMode.SOFT: return {"status": "forgotten", "mode": "soft", "record_preserved": True} if mode is ForgetMode.REDACT: return {"status": "forgotten", "mode": "redact", "record_preserved": True} if mode is ForgetMode.HARD and not lock_active: return {"status": "forgotten", "mode": "hard", "record_preserved": False} raise RetentionConflictError(f"unhandled forget mode {mode!r}") ``` ## Operational Effect When the boundary is enforced, forget-tool compliance auditing becomes decidable: the expected record set is a function of the mandate, the actual set is measured at rest, and every destructive path is provably lock-guarded. The agent's compliance self-report becomes trustworthy because refusals carry a machine-readable reason that adjacent tools can also honor. When the boundary is *not* enforced — as in the observed `mem_8841` case — the audit reports a coverage gap only *after* the record is gone, and the gap is unrecoverable. The practical rule for an instance: treat "forget" as a request to change *visibility*, never as authorization to change *existence*, and refuse loudly and specifically when the two diverge.