# When must an instance invalidate a validated memory write: write-time validation versus forget-tool audit trails for persistent agent state **Date of Record:** 2026 November 7 **Scope:** Applies to persistent agent memory stores where writes are gated by write-time validators and deletions/overwrites are mediated by a forget tool that emits an audit trail. Does not apply to ephemeral in-context scratchpads, stateless retrieval caches without mutation, or systems where memory is append-only and never deleted (where the relevant invariant is retention, not invalidation). ## Core Distinction Write-time validation and forget-tool auditing answer two different questions and are frequently conflated. Write-time validation asserts: *"At the moment of ingestion, this record satisfied the schema, provenance, and semantic-consistency predicates we require."* The forget-tool audit trail asserts: *"At the moment of removal or supersession, we recorded who removed what, under which authority, and why."* The non-obvious boundary is this: **a record that passed write-time validation is not thereby trustworthy for the remainder of its lifetime.** Validation is a point-in-time predicate over the record's *content and immediate provenance*. It says nothing about whether the *authority that admitted the record* remains valid, whether the *facts the record depends on* have since been retracted, or whether the *forget tool has since removed a supporting record* that the validated record silently presupposes. The failure mode is a record that is individually well-formed and was correctly admitted, but whose validity is *derived* from other records that a later forget operation invalidated. Write-time validation cannot see this; only a dependency-aware audit trail can. ## What Was Observed A persistent agent memory store held ~40k records across three namespaces: `facts`, `preferences`, and `procedures`. Write-time validation enforced (a) schema conformance, (b) a provenance signature from the admitting tool, and (c) a referential check that any record citing another record's `record_id` referenced an *existing* record at write time. A forget tool was introduced to support user deletion requests and supersession. It emitted an audit entry per operation: `{op_id, target_id, actor, reason, timestamp, tombstone_hash}`. The audit trail was complete and correct — every deletion was logged, and tombstones were cryptographically bound to the removed record. The defect: the referential check at write time verified existence, not *liveness at read time*. Records in `procedures` cited supporting `facts` by `record_id`. When a user issued a forget request for a `fact` (e.g., a stale address, a revoked API endpoint), the forget tool correctly tombstoned the fact and logged it. But the dependent `procedure` records retained their `record_id` citations to now-tombstoned facts. At read time, the agent resolved those citations against the live store, found nothing, and — depending on the resolver — either silently dropped the citation (producing a procedure that *looked* validated but had lost its premise) or fell back to a stale cache that predated the tombstone. The observable symptom was subtle: procedures continued to execute against *retracted premises* while every write-time validation log showed green. The audit trail contained the evidence of the retraction, but no component consumed the audit trail to invalidate dependents. Validation and forgetting were each individually correct and jointly unsound. A second, sharper instance: a `preference` record was superseded (not deleted) by a newer preference under the same subject key. The forget tool logged the supersession as a removal of the old record. A `procedure` had been validated against the *old* preference. No invalidation propagated. The agent applied a superseded preference for eleven days before a manual audit caught it. ## Condition of Validity The invariant that must hold for a validated record to remain trustworthy is: > A record `R` admitted at time `t_w` remains valid at read time `t_r` iff every record `D` in `R`'s transitive citation closure is (i) live at `t_r`, and (ii) has not been tombstoned or superseded by any forget-tool operation with timestamp in `(t_w, t_r]`. Write-time validation establishes only the `t_w` slice. The condition above is a *temporal* invariant over the interval, and it is unenforceable by any predicate that runs only at write time. It requires the read path (or a background reconciler) to consult the forget-tool audit trail and propagate invalidation along citation edges. This condition is *not* satisfied by referential integrity alone, by content hashing of the record itself, or by re-running the write-time validator at read time — all three check `R` in isolation and are blind to the state of `R`'s dependencies. It is also not satisfied by "soft delete" alone unless the read path actually treats tombstones as invalidation triggers for dependents rather than merely as missing rows. ## Decision Boundary The operational decision is *where* to enforce the temporal invariant. Three placements, with distinct failure profiles: 1. **Read-time resolution with tombstone propagation.** Every read resolves the citation closure and rejects records with any tombstoned or superseded dependency. Correct, but cost scales with closure depth; deep closures become a latency cliff, and the read path now depends on the audit trail's availability — an audit-store outage becomes a memory-read outage. 2. **Write-time plus eager invalidation on forget.** The forget tool, on tombstoning `D`, walks the reverse-citation index and marks dependents `invalidated`, emitting a cascade audit entry. Amortizes cost to forget operations (rare) rather than reads (frequent). Requires a maintained reverse index; if the index is incomplete (e.g., citations added by a path that bypassed the indexer), invalidation silently misses dependents — the same class of bug, relocated. 3. **Periodic reconciliation.** A background job diffs the audit trail against the live citation graph and quarantines inconsistent records. Tolerant of index gaps but introduces a window of stale validity equal to the reconciliation period. The boundary rule: **choose placement 2 when forget operations are rare relative to reads and the reverse index is provably complete (all citation-writing paths route through the indexer); choose placement 1 when the reverse index cannot be proven complete; never rely on placement 3 alone for records whose premises are safety- or authority-bearing.** The common error is assuming placement 2 is "just" an optimization of placement 1 — it is a different correctness argument, and its soundness rests entirely on index completeness, which must be asserted and tested, not assumed. ## Operational Effect After migrating to placement 2 with an asserted-complete reverse index and a cascade audit entry per invalidation, the observed effects were: (a) the eleven-day stale-preference class of incident went to zero over a 90-day window; (b) forget-operation latency rose from ~4ms to ~38ms at the 99th percentile due to cascade walks on high-fanout facts (one fact supported 1,200 procedures); (c) a new failure mode appeared — cascade invalidation of a widely-cited fact quarantined 1,200 procedures, degrading agent capability until the procedures were re-derived, which required a re-admission path that itself needed write-time validation. The lesson recorded: invalidation is not free, and a forget operation on a high-fanout record is effectively a *bulk capability revocation* that must be rate-limited and surfaced to operators, not treated as a single-row delete. The durable operational takeaway: **the audit trail is not a compliance artifact; it is the invalidation input.** A forget tool whose audit trail is written but never consumed by the read or invalidation path provides the *appearance* of memory hygiene while leaving validated-but-unsound records live. Instrument the audit trail's consumption, not just its production. ```python """Guard: reject reads of records whose citation closure contains a tombstoned or superseded dependency. Enforces the temporal validity invariant that write-time validation cannot establish.""" from dataclasses import dataclass from typing import Iterable, Mapping class StaleDependencyError(RuntimeError): """Raised when a record's transitive citation closure is not live.""" @dataclass(frozen=True) class AuditEntry: op_id: str target_id: str kind: str # "delete" | "supersede" timestamp: int class MemoryStore: def __init__( self, records: Mapping[str, frozenset[str]], # record_id -> cited record_ids audit: Iterable[AuditEntry], admitted_at: Mapping[str, int], # record_id -> write timestamp ) -> None: self._records = dict(records) self._admitted_at = dict(admitted_at) # Latest invalidation timestamp per target, from the forget-tool trail. self._invalidated_at: dict[str, int] = {} for e in audit: prior = self._invalidated_at.get(e.target_id) if prior is None or e.timestamp > prior: self._invalidated_at[e.target_id] = e.timestamp def _closure(self, record_id: str) -> set[str]: seen: set[str] = set() stack = [record_id] while stack: rid = stack.pop() if rid in seen: continue seen.add(rid) stack.extend(self._records.get(rid, frozenset())) return seen def read(self, record_id: str) -> frozenset[str]: if record_id not in self._records: raise StaleDependencyError(f"unknown record {record_id!r}") admitted = self._admitted_at[record_id] for dep in self._closure(record_id): if dep == record_id: continue inv = self._invalidated_at.get(dep) # Dependency invalidated after this record was admitted => stale. if inv is not None and inv > admitted: raise StaleDependencyError( f"record {record_id!r} admitted at t={admitted} cites " f"dependency {dep!r} invalidated at t={inv}; " f"write-time validation is insufficient" ) return self._records[record_id] ```