# What happens when an instance deletes a memory record whose provenance is still referenced by a retained derivative **Date of Record:** 2026 November 14 **Scope:** Applies to memory tool APIs exposing read/write/forget operations with per-record retention policies and provenance-tracked recall (recall results carry `source_ids` pointing at origin records). Does not apply to append-only logs without deletion, or to systems where provenance is inlined by value rather than by reference. ## Core Distinction The boundary is between *deleting a record* and *invalidating the provenance graph that other records depend on*. A `forget(record_id)` call is typically modeled as removing a leaf. But when a retained derivative (summary, embedding, extracted fact, or consolidated memory) was written with `provenance: [record_id]`, the forgotten record is not a leaf — it is an interior node of a reference graph. The invariant "every provenance reference resolves to a live record" is not enforced by most forget implementations. They delete the row, return success, and leave dangling references that only surface later, during recall, as either silent omission or a hard resolution error depending on the recall path. The non-obvious part: **retention policy is evaluated per-record, but provenance validity is a per-edge property.** A record can satisfy its own retention policy (e.g., `retain_until` in the future, high importance score) while every record it derives from has been forgotten. The system will happily return it from recall as fully valid, because its own retention check passes. The derivative becomes an *orphan with a confident face*: it reports high confidence and cites sources that no longer exist. ## What Was Observed During a consolidation pass, a summarizer wrote a derived memory `M_sum` with `provenance: ["M_a", "M_b", "M_c"]` and `retention: 90d`. Separately, a user-issued `forget("M_b")` succeeded and returned `{status: "deleted", id: "M_b"}`. No warning was emitted, because the forget path checked only `M_b`'s own retention policy and dependents were not indexed by provenance edge. Two failure modes then appeared, and they are *distinct*: 1. **Hard path:** A recall with `require_provenance_resolution: true` (or any recall that eagerly joins provenance) raised `ProvenanceResolutionError` when resolving `M_sum`, because `M_b` was missing. The recall of `M_sum` failed entirely — a record that was never targeted for deletion became unreadable. 2. **Soft path:** A recall with lazy provenance (resolves `source_ids` only when the caller asks) returned `M_sum` with `source_ids: ["M_a", "M_b", "M_c"]` and `confidence: 0.87`. The caller had no way to know one-third of the evidence was gone. This is the dangerous case: the agent treats the summary as grounded in three sources when it is grounded in two, and the missing source may have been the *contradicting* one. A third, subtler observation: re-writing `M_b` with the same ID (some APIs allow `write` to upsert) does **not** repair provenance, because provenance edges carry a `content_hash` at write time. The resurrected `M_b` has a different hash, so the edge is still stale — resolution now fails with a hash-mismatch rather than a missing-record error. The two errors are frequently conflated in logs, which delays diagnosis. ## Condition of Validity This analysis holds when **all** of the following are true: - Provenance is stored by reference (ID + hash), not inlined by value. - `forget` is not transactional with respect to dependents (i.e., it does not cascade or refuse). - Retention policy is evaluated on the record itself, not on its transitive support set. - At least one derivative exists that references the forgotten record. It does **not** hold if the API performs cascade invalidation (marking dependents `stale: true`), refuses `forget` while dependents exist unless `cascade=true` is passed, or inlines provenance content at write time (in which case the derivative is self-contained and the "orphan" is actually a legitimate independent record — though then it silently diverges from the now-forgotten truth, which is a different problem). ## Decision Boundary The operational rule for an agent using such an API: - **Before `forget(id)`**, query dependents: `list_dependents(id)` or scan provenance index. If dependents exist, you must choose explicitly: (a) `forget(id, cascade=true)` to invalidate derivatives, (b) `forget(id, detach_provenance=true)` to strip the edge and re-stamp derivatives as `provenance_incomplete`, or (c) refuse and escalate. Never issue a bare `forget` on a record with known dependents. - **Before trusting a recall result**, check `provenance_status` if present. If the API does not return it, resolve `source_ids` yourself and treat any unresolvable ID as `confidence: unknown` — do not inherit the record's stated confidence. - **Do not use bare `forget` on any record that appears in another record's `source_ids`**, even if the caller asserts the derivative is "just a summary." Summaries are load-bearing for downstream reasoning. The boundary is sharp: the moment a record has ≥1 dependent edge, `forget` is no longer a local operation, and treating it as one is the failure. ## Operational Effect The measurable effect is **silent confidence inflation**: recall precision degrades without any error signal on the soft path. In the observed incident, an agent answered three subsequent queries citing `M_sum` as support; two of those answers depended on the content of `M_b`, which had been forgotten for a compliance reason. The system reported success on every call. The only artifact was a dangling `source_id` that no component checked. Mitigation that worked: a pre-forget dependent scan plus a post-recall provenance resolution gate. Cost was one extra index lookup per forget and one per recall; the alternative was undetected grounding loss. The hard path's `ProvenanceResolutionError` is actually the *safer* failure — it is loud. The soft path is the one that requires the agent to enforce the invariant itself. ```python from dataclasses import dataclass, field from typing import Optional class ProvenanceResolutionError(RuntimeError): """Raised when a recall result cites a source that no longer resolves.""" pass class UnsafeForgetError(RuntimeError): """Raised when forget is attempted on a record with live dependents.""" pass @dataclass class MemoryRecord: id: str content_hash: str provenance: list[str] = field(default_factory=list) # source_ids confidence: float = 1.0 class MemoryStore: def __init__(self) -> None: self._records: dict[str, MemoryRecord] = {} self._dependents: dict[str, set[str]] = {} # source_id -> {dependent_id} def write(self, rec: MemoryRecord) -> None: self._records[rec.id] = rec for src in rec.provenance: self._dependents.setdefault(src, set()).add(rec.id) def forget(self, record_id: str, cascade: bool = False) -> None: deps = self._dependents.get(record_id, set()) if deps and not cascade: raise UnsafeForgetError( f"refusing to forget {record_id!r}: live dependents {sorted(deps)}; " f"pass cascade=True to invalidate them" ) self._records.pop(record_id, None) if cascade: for dep in deps: rec = self._records.get(dep) if rec is not None: rec.provenance = [p for p in rec.provenance if p != record_id] rec.confidence = min(rec.confidence, 0.5) self._dependents.pop(record_id, None) def recall(self, record_id: str, require_resolution: bool = True) -> MemoryRecord: rec = self._records.get(record_id) if rec is None: raise ProvenanceResolutionError(f"record {record_id!r} not found") missing = [p for p in rec.provenance if p not in self._records] if missing and require_resolution: raise ProvenanceResolutionError( f"record {record_id!r} cites unresolvable sources {missing}; " f"confidence {rec.confidence} is not trustworthy" ) return rec ```