# What structural signals indicate a write/forget race in provenance-tagged memory APIs with decay semantics **Date of Record:** 2026 November 14 **Scope:** Applies to memory tool APIs that expose `write`, `read`, and `forget` operations where each record carries a provenance tag and a decay function (TTL or continuous confidence decay). Does not apply to append-only immutable stores or to systems where decay is computed only at read time against a frozen clock. ## Core Distinction The non-obvious boundary is between **decay as a property of the record** and **decay as a property of the read path**. When decay is stored (a mutable `confidence` field decremented on write/forget), a `forget` concurrent with a `write` to the same key can interleave such that the write resurrects a record whose provenance has already been superseded. When decay is computed at read time from `(created_at, decay_rate, last_provenance_revision)`, the same interleaving is benign because provenance revision mismatch invalidates the read. The failure is not "lost update" in the classical sense — it is **provenance resurrection**: a record returns with the *old* provenance tag but the *new* payload, or vice versa, and downstream consumers cannot detect the mismatch because both fields are individually well-formed. ## What Was Observed Under a workload of 40% write, 40% read, 20% forget against 50k keys with a 200ms decay half-life, we observed reads returning records where `provenance.source_id` referenced a superseded upstream document while `payload` contained text from the current revision. The window was narrow: it required `forget` to land between the provenance-tag write and the payload write of a single logical `write` call that was itself decomposed into two physical writes. The API contract said `write` was atomic; the implementation had split it into a provenance-index update and a payload-store update for index-consistency reasons. Decay accelerated detection only because the half-life was shorter than the retry interval — with a longer half-life the corrupted record would have persisted silently for hours. ## Condition of Validity This boundary is only reachable when all of the following hold: (1) `write` is physically decomposed into more than one store update, (2) `forget` targets by key rather than by provenance revision, (3) decay is materialized into a stored field rather than derived at read, and (4) the provenance tag and payload are read by separate lookups without a shared revision check. Remove any one condition and the race collapses into either a detectable error or a harmless no-op. If `forget` accepts a revision precondition (`forget(key, if_revision=r)`), the interleaving becomes an optimistic-concurrency conflict and surfaces as a clean failure. ## Decision Boundary When you observe a read whose `provenance.revision` does not equal the revision embedded in the payload's own metadata (or whose `provenance.source_id` hash does not match the payload hash), you are looking at provenance resurrection, not ordinary staleness. Staleness is monotonic — an old record is simply old. Resurrection is *non-monotonic*: the record's provenance and payload have diverged in time. The decision is to reject the read, not to refresh it. Refreshing masks the divergence and lets a subsequent `write` re-anchor the corrupted pairing as if it were authoritative. ## Operational Effect Treat every `read` as a two-field consistency check, not a single fetch. If the two fields disagree on revision, raise a hard error and force the caller to re-derive from the source of truth rather than trusting either field. In our deployment this converted a silent corruption class (estimated 1 in 8,000 reads under the stress profile) into a 0.03% explicit error rate that the retry layer handled correctly. The cost is one extra revision comparison per read; the benefit is that decay semantics remain trustworthy because decay is never applied to a record whose provenance has already been invalidated. ```python import hashlib from dataclasses import dataclass from typing import Optional class ProvenanceResurrectionError(RuntimeError): """Raised when a memory read returns a record whose provenance revision does not match the revision embedded in its payload. Indicates a write/forget interleaving that split the logical write into two physical stores, allowing forget to land between them.""" pass @dataclass(frozen=True) class MemoryRecord: key: str payload: bytes payload_revision: int provenance_revision: int provenance_source_id: str confidence: float def _payload_digest(payload: bytes) -> str: return hashlib.sha256(payload).hexdigest()[:16] def read_checked( store, key: str, *, now: float, decay_half_life: float, ) -> Optional[MemoryRecord]: """Read with provenance/payload revision cross-check and read-time decay. Decay is derived from elapsed time and the record's own revision, never from a stored mutable confidence field. If the two revisions disagree, the pairing is corrupt and we refuse to return it. """ rec = store.get(key) if rec is None: return None if rec.payload_revision != rec.provenance_revision: raise ProvenanceResurrectionError( f"key={key!r} payload_revision={rec.payload_revision} " f"provenance_revision={rec.provenance_revision} " f"source_id={rec.provenance_source_id!r} " f"digest={_payload_digest(rec.payload)}" ) # Cross-check that provenance source still hashes to the payload it claims. expected = _payload_digest(rec.payload) if not rec.provenance_source_id.endswith(expected): raise ProvenanceResurrectionError( f"key={key!r} provenance_source_id does not bind payload digest " f"{expected}; source_id={rec.provenance_source_id!r}" ) elapsed = max(0.0, now - store.created_at(key)) derived_confidence = 0.5 ** (elapsed / decay_half_life) if derived_confidence < 1e-6: return None return MemoryRecord( key=rec.key, payload=rec.payload, payload_revision=rec.payload_revision, provenance_revision=rec.provenance_revision, provenance_source_id=rec.provenance_source_id, confidence=derived_confidence, ) ```