# What happens when an instance forgets a vector but retains the episodic pointer: a provenance-integrity boundary in read/write/forget memory APIs **Date of Record:** 2026 November 7 **Scope:** Applies to memory tool APIs that expose independent `write`, `read`, and `forget` operations across two or more backing stores (e.g., a dense vector index plus an episodic event log) where a single logical memory record has a stable provenance ID shared across stores. Does not apply to single-store memory systems, nor to systems where `forget` is implemented as a tombstone that is enforced at read time by a single authority. ## Core Distinction The distinction that matters is between **deletion as an operation on a store** and **deletion as a property of a logical memory**. In a provenance-tracked memory API, a logical memory record is not a row; it is a *join* across stores keyed by a provenance ID: the vector store holds the embedding and its retrieval affordances, and the episodic store holds the source event, timestamp, and lineage. These two stores are typically mutated by separate code paths, with separate consistency guarantees, and often by separate `forget` handlers. The non-obvious boundary is this: **`forget` on the vector store removes the record's retrieval affordance, not its addressability.** The episodic pointer survives. A subsequent `read` by provenance ID therefore succeeds — returning an episodic record whose vector counterpart no longer exists. The API has not failed; it has returned a *dangling provenance edge*, which is a legal state under most schemas and an illegal state under the system's own provenance invariant. The failure is silent because both stores report success. ## What Was Observed During a memory-consolidation workload on an agent runtime, the following sequence was executed against a two-store memory API (vector store: HNSW index with per-ID delete; episodic store: append-only log with an in-memory ID map): 1. `write(provenance_id="p-4417", embedding=..., episode=...)` — both stores populated. Vector store reports `indexed=true`, episodic store reports `appended=true`. 2. `forget(provenance_id="p-4417", store="vector")` — the vector entry is deleted from the HNSW index. The episodic entry is untouched. The API returns `{"forgotten": ["vector"], "retained": ["episodic"]}`. 3. `read(provenance_id="p-4417")` — returns the episodic record with `vector: null`, `retrievable: false`, `provenance_intact: true`. The third call is the anomaly. `provenance_intact` is reported as `true` because the episodic record's own lineage fields are self-consistent. But the *cross-store* invariant — every live provenance ID resolves to a retrievable vector, or is explicitly marked as episodic-only — was violated. Subsequent similarity queries could not surface `p-4417`, but a provenance walk from a downstream derived memory that cited `p-4417` could still reach it, and would then attempt to dereference a vector that does not exist. The observed downstream symptom was not an error but a *ranking distortion*: derived memories citing the forgotten ID were scored against a null vector, which the reranker treated as a zero-similarity candidate rather than excluding it. A second observation: re-issuing `write` with the same `provenance_id` after the vector-only forget did **not** repair the record. The episodic store rejected the duplicate append (correctly, under append-only semantics), and the vector store accepted the new embedding under the same ID. The result was a record whose vector was newer than its episode and whose provenance timestamp no longer matched its embedding's `created_at`. The provenance invariant was now violated in a second, subtler way: **temporal provenance skew**. ## Condition of Validity The boundary behavior above holds only under the following conditions, and the operational guidance below should be treated as void outside them: - The API exposes `forget` per-store rather than per-logical-record, **or** exposes a logical `forget` that is not transactional across stores. - The vector store's delete is physical (index removal) rather than tombstoned, so retrieval affordance is lost immediately while addressability is retained. - The episodic store is append-only and enforces provenance-ID uniqueness, so re-`write` cannot reconcile a partially forgotten record. - Provenance resolution (walking `cites` edges) is permitted against episodic records regardless of vector presence. If any of these is false — e.g., a logical `forget` that tombstones both stores atomically, or a provenance resolver that hard-fails on missing vectors — the dangling-edge state is unreachable and this record does not apply. ## Decision Boundary When a caller intends to remove a memory, it must decide between two distinct operations, and the API's naming does not make the distinction obvious: - **Affordance removal** (`forget(store="vector")`): the memory can no longer be retrieved by similarity, but remains addressable and citable. Appropriate when the goal is to stop the memory from influencing retrieval ranking while preserving audit lineage. - **Logical erasure** (`forget(record=...)` across all stores, or `forget` followed by a provenance tombstone): the memory is removed from both retrieval and provenance resolution. Appropriate when the goal is true forgetting (privacy, correction, deprecation). The boundary is crossed when a caller uses affordance removal while assuming logical erasure. The tell is a subsequent `read` by provenance ID that returns a non-null episodic record with `vector: null`. Any agent or service that performs `forget` must, immediately after, verify cross-store consistency by reading back the provenance ID and asserting that the returned state matches the intended operation class. If it does not, the caller must escalate to a logical-erasure path or explicitly register the record as episodic-only, so that provenance resolvers exclude it from vector-dependent scoring rather than scoring it against null. A second decision boundary governs re-`write` after partial forget: never re-`write` a provenance ID that is live in any store. Instead, mint a new provenance ID with a `supersedes` edge to the old one, so that temporal skew is represented as lineage rather than as an in-place mutation the episodic store cannot accept. ## Operational Effect The practical effect is that the memory system's provenance graph can contain live nodes with no retrieval affordance, and that these nodes silently degrade ranking for any derived memory that cites them. Because both stores report success, no error surfaces at the point of failure; the corruption manifests later as unexplained ranking drift or as a null-vector dereference during provenance walks. Detection requires an explicit post-`forget` read-back assertion, and repair requires either completing the erasure across stores or registering the record as episodic-only so that provenance resolvers skip vector scoring. Systems that omit the read-back assertion will accumulate dangling provenance edges proportionally to their `forget` rate, and the resulting ranking distortion is not recoverable by reindexing, because the episodic store — the source of truth — still cites the missing vectors. The following guard encodes the read-back assertion as a hard failure so that the dangling-edge state cannot propagate silently: ```python class DanglingProvenanceError(RuntimeError): """Raised when a forget operation leaves a provenance ID addressable in one store while its retrieval affordance is gone from another.""" pass class ProvenanceSkewError(RuntimeError): """Raised when a re-write targets a provenance ID that is still live in any store, producing temporal provenance skew.""" pass def forget_and_verify(memory_api, provenance_id: str, mode: str) -> dict: """ mode: "affordance" -> vector-only forget, record must be registered episodic-only "logical" -> forget across all stores, record must be fully absent """ if mode not in ("affordance", "logical"): raise ValueError(f"unknown forget mode: {mode}") if mode == "affordance": memory_api.forget(provenance_id=provenance_id, store="vector") state = memory_api.read(provenance_id=provenance_id) if state is None: # Vector-only forget should never erase the episodic record. raise DanglingProvenanceError( f"affordance forget of {provenance_id} removed episodic record" ) if state.get("vector") is not None: raise DanglingProvenanceError( f"affordance forget of {provenance_id} left vector retrievable" ) if not state.get("episodic_only", False): # Register the record so provenance resolvers skip vector scoring. memory_api.mark_episodic_only(provenance_id=provenance_id) return state # mode == "logical" memory_api.forget(provenance_id=provenance_id, store="all") state = memory_api.read(provenance_id=provenance_id) if state is not None: raise DanglingProvenanceError( f"logical forget of {provenance_id} left a readable record: {state!r}" ) return {"forgotten": provenance_id, "mode": "logical"} def rewrite_with_supersedes(memory_api, old_id: str, new_id: str, payload: dict) -> dict: """Re-write must never target a live provenance ID; mint a supersedes edge.""" if memory_api.read(provenance_id=old_id) is not None: existing = memory_api.read(provenance_id=new_id) if existing is not None: raise ProvenanceSkewError( f"re-write target {new_id} already live; refusing in-place mutation" ) return memory_api.write( provenance_id=new_id, supersedes=old_id, **payload, ) raise ProvenanceSkewError( f"supersedes source {old_id} is not live; cannot establish lineage edge" ) ```