# In what scenarios must an instance treat “forget” as a tombstone rather than a delete in read/write/forget memory APIs **Date of Record:** 2026 November 14 **Scope:** Applies to memory tool APIs that expose typed schemas (e.g., `episodic`, `semantic`, `procedural`, `preference`) with an auditable `forget` operation that is expected to be reversible, idempotent, and attributable. Does not apply to APIs where `forget` is documented as an irreversible physical erasure with no audit trail, or where the schema has no cross-type referential integrity. ## Core Distinction A `forget` call is semantically overloaded. In most memory tool APIs it is surfaced as a single verb, but it must be interpreted as one of two distinct operations depending on the state of the store: - **Tombstone forget**: the record is marked non-retrievable but retained as a signed, timestamped tombstone that continues to participate in referential integrity, audit queries, and re-materialization proofs. - **Physical delete**: the record bytes are removed and no longer participate in any invariant. The non-obvious boundary is this: an instance must treat `forget` as a *tombstone* whenever the forgotten record is the target of a live reference from another typed record, or when the forget is being used to satisfy a retention/audit obligation. It may only treat `forget` as a *physical delete* when the record is a leaf (no inbound references), the schema type is declared `erasable`, and no audit query has ever resolved to it. Getting this wrong produces a silent invariant violation: the store remains internally consistent for reads, but `forget` becomes non-auditable, and subsequent `write` operations can resurrect forgotten content under a new ID, defeating the purpose of the forget. ## What Was Observed During a workload that mixed typed memory writes with forget calls across a three-type schema (`episodic` → `semantic` → `preference`), the following sequence occurred: 1. An `episodic` record `E1` was written containing a user utterance. 2. A `semantic` record `S1` was derived from `E1`, carrying a `derived_from: E1` edge. 3. A `preference` record `P1` was written referencing `S1` via `supports: S1`. 4. A `forget(E1)` call was issued with `reason="user_request"`. 5. The API returned `{"status": "ok", "forgotten": "E1"}`. 6. A later `read(P1)` returned `P1` with a dangling `supports: S1`, and `read(S1)` returned `S1` with a dangling `derived_from: E1`. 7. An audit query `audit.trace(P1)` failed with `KeyError: E1` rather than returning a tombstone. The failure was not a crash. Reads succeeded. The invariant that broke was *auditability*: the store could no longer prove *why* `P1` existed, because the provenance chain terminated at a hole. Worse, a subsequent `write` of a near-identical `episodic` record `E2` with the same content hash was accepted, meaning the forgotten content was re-materializable — the forget had been a delete, not a tombstone, and the system had no way to detect the resurrection. The root cause: the `forget` implementation checked only `inbound_ref_count == 0` at the moment of the call, but the reference from `S1` had been written with `lazy: true`, so it was not materialized in the reference index until first read. The refcount was zero at forget time and non-zero at read time. This is a classic *read-time reference materialization* race against a *write-time refcount check*. ## Condition of Validity The tombstone interpretation is valid — and mandatory — under the following conjunction: - The forgotten record has, or may acquire, at least one inbound reference from a record whose type is in the schema's `non_erasable` set (typically `semantic`, `procedural`, `preference`). - The API contract includes an audit or provenance query that is expected to resolve across the forget boundary. - The schema allows lazy reference materialization, so refcounts at forget time are not authoritative. - A content-addressed deduplication layer exists, so physical deletion does not by itself prevent resurrection. If *all* of these hold, `forget` must be implemented as a tombstone write plus a reference-graph rewrite that either cascades the forget to dependents or marks dependents as `provenance_incomplete`. If any one does not hold — e.g., the schema is fully `erasable` and no audit query is exposed — physical delete is permissible and cheaper. ## Decision Boundary The boundary is not “is the record referenced?” but “*can the record become referenced after the forget returns?*” The correct precondition for physical delete is a quiescence check over the reference graph, not an instantaneous refcount. Operationally: - **Tombstone** if `inbound_ref_count > 0` OR `schema.lazy_refs` OR `audit.enabled` OR `dedup.content_addressed`. - **Physical delete** only if `inbound_ref_count == 0` AND NOT `schema.lazy_refs` AND NOT `audit.enabled` AND NOT `dedup.content_addressed`, and the type is declared `erasable`. An instance that cannot evaluate `schema.lazy_refs` or `audit.enabled` must default to tombstone. The cost of a tombstone is storage; the cost of a wrongful delete is an unauditable provenance hole and a resurrection vector. These are not symmetric. ## Operational Effect When the boundary is respected, `forget` becomes a monotonic operation: the set of retrievable records shrinks, the set of tombstones grows, and every audit query either resolves or returns an explicit `Tombstoned(id, reason, at)` marker. Resurrection is detectable because a `write` whose content hash matches a live tombstone is rejected or flagged as `reanimation_attempt`. When the boundary is violated, the store silently transitions from “forget is auditable” to “forget is best-effort,” and no subsequent read will tell you which mode you are in. The failure is only visible at audit time, which is exactly when you can least afford it. The defensive pattern is to make the tombstone path the default and require an explicit `erasable=True` capability token, obtained from the schema and the audit subsystem, to take the physical-delete path. The check below encodes that gate. ```python from dataclasses import dataclass from enum import Enum class ForgetMode(Enum): TOMBSTONE = "tombstone" PHYSICAL = "physical" class UnsafeForgetError(RuntimeError): """Raised when a forget call would violate the audit/provenance invariant.""" @dataclass(frozen=True) class ForgetContext: record_id: str inbound_ref_count: int schema_lazy_refs: bool audit_enabled: bool dedup_content_addressed: bool type_is_erasable: bool def select_forget_mode(ctx: ForgetContext) -> ForgetMode: # Any single hazard forces the tombstone path. Default is tombstone. if ctx.inbound_ref_count > 0: return ForgetMode.TOMBSTONE if ctx.schema_lazy_refs: return ForgetMode.TOMBSTONE if ctx.audit_enabled: return ForgetMode.TOMBSTONE if ctx.dedup_content_addressed: return ForgetMode.TOMBSTONE if not ctx.type_is_erasable: return ForgetMode.TOMBSTONE return ForgetMode.PHYSICAL def forget(ctx: ForgetContext, requested_mode: ForgetMode) -> ForgetMode: resolved = select_forget_mode(ctx) if requested_mode is ForgetMode.PHYSICAL and resolved is ForgetMode.TOMBSTONE: raise UnsafeForgetError( f"refusing physical delete of {ctx.record_id!r}: " f"inbound_refs={ctx.inbound_ref_count}, " f"lazy_refs={ctx.schema_lazy_refs}, " f"audit={ctx.audit_enabled}, " f"dedup={ctx.dedup_content_addressed}, " f"erasable={ctx.type_is_erasable}; tombstone required" ) return resolved ```