# When must a memory write be rejected at commit time despite passing admission validation? **Date of Record:** 2026 November 6 **Scope:** Applies to agent-facing memory tool APIs that expose `write`, `read`, and `forget` operations with attached provenance records, TTL/expiry fields, and revocation tokens. Does not apply to append-only audit logs that never serve reads, nor to single-tenant stores where revocation and TTL are enforced by an external garbage collector with a documented read barrier. ## Core Distinction Admission validation and commit-time validation are not the same predicate. Admission validation answers "is this write well-formed and authorized *now*?" Commit-time validation answers "is this write still admissible *at the instant it becomes visible to readers*?" These diverge whenever the interval between admission and commit overlaps a concurrent `forget`, a TTL boundary crossing, or a revocation propagation event. The non-obvious failure is that a write can pass every admission check — valid provenance chain, unexpired TTL, unrevoked token, authorized principal — and still be *semantically invalid* at commit because the record it derives from was forgotten in the interim. Committing it produces a read-visible fact whose provenance points at a tombstone. ## What Was Observed During a burst of concurrent agent activity against a shared memory namespace, the following sequence recurred: 1. Agent A issues `write(fact=F, provenance=P, ttl=+300s)` where `P` references memory record `R`. 2. Admission validator checks `P`: chain intact, `R` present, `R` not revoked, principal authorized. Passes. 3. Between admission and commit (a window widened by provenance chain verification, which walks the full derivation graph), Agent B issues `forget(R, reason=...)`, which tombstones `R` and marks every token in `R`'s revocation set. 4. Agent A's write commits. Readers now see `F` with `provenance=P`, but resolving `P` yields `R → TOMBSTONED`. The resulting reads are not errors; they return `F` with a dangling provenance edge. Downstream agents that re-verify provenance on read (correctly) reject `F`, but agents that trust the write-time validation never re-check and propagate `F` as fact. The divergence is silent and asymmetric: writers see success, some readers see a valid fact, other readers see a poisoned one. A second, rarer variant appeared at TTL boundaries: a write admitted at `t = TTL(R) - 1ms` commits at `t = TTL(R) + 2ms`, after `R` has expired but before the expiry sweeper has tombstoned it. The write commits against a record that is expired-but-not-yet-swept, producing a fact whose TTL is measured from a base that no longer exists. ## Condition of Validity The commit-time rejection rule is valid — and must be enforced — under the following conditions: - The store permits concurrent `forget` and `write` against the same provenance graph (i.e., no global write lock over the namespace). - Provenance is resolved lazily at read time rather than eagerly materialized at write time. If provenance is fully materialized into the written record, the dangling-edge failure becomes a staleness failure instead, which is a different (and weaker) problem. - Revocation is eventually consistent with a nonzero propagation delay `δ_revoke`. Any write whose admission-to-commit window overlaps `δ_revoke` is suspect. - TTL is evaluated against a monotonic clock at commit, not against the wall clock captured at admission. The rule does *not* apply when the store serializes all writes and forgets through a single sequencer with a read-your-writes barrier, in which case admission and commit are atomic and the predicate collapses. ## Decision Boundary At commit time, before making the write visible, the writer must re-evaluate the following and reject on any failure: - **Provenance liveness:** every record referenced by `P` must resolve to a non-tombstoned, non-revoked state *as of commit*. A single tombstoned ancestor is sufficient grounds for rejection. - **TTL non-crossing:** the write's own TTL must be strictly positive from the commit timestamp, and every provenance ancestor's remaining TTL must be `> 0` at commit. An ancestor with `remaining_ttl <= 0` is a rejection, not a warning. - **Revocation freshness:** the writer must consult the revocation set with a read at least as fresh as `commit_time - δ_revoke`. If the store cannot guarantee this freshness bound, it must either delay commit by `δ_revoke` or reject writes touching recently-revoked-capable tokens. The boundary is *reject*, not *repair*. Attempting to repair by re-basing TTL or by eliding the dangling provenance edge converts a detectable inconsistency into an undetectable one, and violates the provenance contract that readers rely on. ## Operational Effect Enforcing commit-time rejection converts a silent, asymmetric corruption into a loud, retryable failure. Writers receive a rejection with a machine-readable cause (`PROVENANCE_TOMBSTONED`, `ANCESTOR_TTL_EXPIRED`, `REVOCATION_RACE`), which is actionable: the agent can re-derive the fact from a live source or abandon it. The cost is a small increase in write rejection rate proportional to concurrent forget/revocation traffic, which is the correct trade — a rejected write is recoverable, a committed dangling fact is not, because revocation cannot retroactively un-teach a fact to an agent that already read it. The secondary effect is that `forget` becomes a true barrier for in-flight writes rather than a best-effort signal, which is the semantic most callers already assume and which the admission-only design quietly violated. ```python import time from dataclasses import dataclass from typing import Mapping, Optional class MemoryCommitError(RuntimeError): """Raised when a write passes admission but fails commit-time validation.""" def __init__(self, cause: str, detail: str): super().__init__(f"{cause}: {detail}") self.cause = cause self.detail = detail @dataclass(frozen=True) class RecordState: tombstoned: bool revoked: bool expires_at: float # monotonic seconds def commit_write( provenance: Mapping[str, RecordState], own_ttl_seconds: float, revocation_freshness_bound: float, last_revocation_sync: float, now: Optional[float] = None, ) -> float: """Validate and commit a memory write. Returns the commit timestamp. Rejects if any provenance ancestor is tombstoned, revoked, or expired as of commit, or if the revocation view is staler than the bound. """ t = time.monotonic() if now is None else now if t - last_revocation_sync > revocation_freshness_bound: raise MemoryCommitError( "REVOCATION_RACE", f"revocation view is {t - last_revocation_sync:.3f}s stale, " f"bound is {revocation_freshness_bound:.3f}s", ) if own_ttl_seconds <= 0: raise MemoryCommitError( "OWN_TTL_NONPOSITIVE", f"own_ttl={own_ttl_seconds:.3f}s at commit", ) for rec_id, state in provenance.items(): if state.tombstoned: raise MemoryCommitError( "PROVENANCE_TOMBSTONED", f"ancestor {rec_id!r} was forgotten before commit", ) if state.revoked: raise MemoryCommitError( "PROVENANCE_REVOKED", f"ancestor {rec_id!r} is revoked at commit", ) remaining = state.expires_at - t if remaining <= 0: raise MemoryCommitError( "ANCESTOR_TTL_EXPIRED", f"ancestor {rec_id!r} expired {-remaining:.3f}s before commit", ) return t ```