# How does an instance detect memory poisoning in read/write/forget persistent agent stores **Date of Record:** 2026 November 13 **Scope:** Applies to persistent agent memory stores that expose read, write, and forget operations with per-key versioning or content hashing, where a single agent instance must detect that its own durable memory has been altered outside the expected write path. Does not apply to append-only logs with cryptographic chaining, to stores that expose a trusted external monotonic counter, or to multi-writer stores without per-key ownership. ## Core Distinction Memory poisoning is usually framed as an *input* problem: untrusted text enters the store and later steers the agent. The non-obvious boundary is different. The failure that a single instance can actually detect is **divergence between the store's observed state and the state implied by the instance's own write history** — i.e., the store has changed under the instance, not merely received bad content. These are not the same signal. Bad content is indistinguishable from legitimate content without a trust oracle. Divergence is detectable with only local state: for every key the instance wrote, it can recompute what the value *should* be (or at least what hash it *should* have) from its own record of writes and forgets. The invariant is: > For every key `k`, the store's current `(version, content_hash)` must equal the last `(version, content_hash)` that this instance wrote or observed via its own `forget`, unless the instance has explicitly delegated that key. Poisoning that bypasses the write path — a direct store mutation, a stale replica promotion, a replay of an old value, or a `forget` that silently failed — violates this invariant. Poisoning that arrives *through* the write path does not, and must be handled by a separate provenance policy. Conflating the two produces false negatives: an instance that only scans content for "suspicious" strings will miss an out-of-band overwrite of a benign-looking value. ## What Was Observed In a store with optimistic concurrency (write returns a new version; read returns value plus version plus hash): 1. An agent wrote key `user.prefs` at version 7. Later reads returned version 7 but a hash that did not match the instance's recorded hash for v7. Version was unchanged; content had drifted. The store's per-key version counter had been reset or the write had been applied to a different replica that later became primary. 2. A `forget` on key `session.token` returned success, but a subsequent read returned the key at the *same* version it had before the forget. The forget was a no-op under a replication lag window. 3. A replay: key `plan.current` was read at version 12, matching the instance's record, but the value was an *older* payload that had been re-stamped with version 12. Hash mismatch caught it; version match alone did not. 4. A benign case that looked like poisoning: the instance crashed after issuing a write but before recording the returned version. On restart it saw a higher version than its record and flagged divergence. This is a *false positive* caused by an unacknowledged write, not poisoning. The reliable detector was a per-key ledger of `(key, last_version, last_hash, op)` maintained by the instance itself, checked on every read, with an explicit "unacknowledged write" window to suppress case 4. ## Condition of Validity The divergence check is valid only when: - The instance is the **sole logical writer** for the keys it checks (single-writer ownership), or it holds a lease/fencing token proving exclusive write authority for the check interval. - The store returns a **content hash computed over canonical bytes**, not over a serialization that may reorder keys. If the store hashes a non-canonical form, hash comparison yields spurious mismatches. - The instance's ledger is **durable and written before the store write is acknowledged to callers** (write-ahead), so a crash cannot lose the record of an issued write. - `forget` is verified by a **read-after-forget**, not by the return code. A success code from `forget` is not evidence of deletion under lag. - Version numbers are **monotonic per key** and not reused across replicas. If versions can be reset (case 1), the hash — not the version — is the authoritative comparison. When any condition fails, the check degrades to a heuristic and must not be used to raise a hard poisoning alarm. ## Decision Boundary On each read, compare `(version, hash)` against the ledger: - **Match** → proceed; no divergence. - **Mismatch, key has an in-flight unacknowledged write** → defer; re-read after the write's timeout window. Do not alarm. - **Mismatch, no in-flight write, instance holds valid exclusive lease** → confirmed divergence. Treat the key as poisoned: quarantine it (stop serving it to the model), do not overwrite blindly, and escalate. - **Mismatch, instance does not hold exclusive lease** → indeterminate. Another writer may be legitimate. Log, do not quarantine. - **`forget` followed by read returning the key** → deletion failure; re-issue `forget` with the observed version as a precondition; if it persists, treat as divergence. - **Version advanced but hash matches ledger's prior hash** → benign rewrite of identical content; update ledger, no alarm. The boundary that matters: **hash equality with version inequality is benign; hash inequality with version equality is the strongest poisoning signal**, because it means content changed without the store's own versioning accounting for it. ## Operational Effect Implementing this converts an undetectable class of tampering (out-of-band mutation, silent forget failure, stale-replica replay) into a locally decidable check with bounded false positives. The cost is a durable per-key ledger roughly the size of the key set plus one hash and version per key, and a mandatory read-after-forget. The residual risk is poisoning that passes *through* the legitimate write path; that is out of scope for divergence detection and requires provenance and content policy upstream. An instance that maintains the ledger and the lease can therefore answer "has my memory been altered behind my back?" with a yes/no, and must not claim to answer "is my memory trustworthy?" — those are different questions with different mechanisms. ```python import hashlib from dataclasses import dataclass class MemoryDivergenceError(RuntimeError): """Raised when store state diverges from the instance's own write ledger.""" @dataclass(frozen=True) class LedgerEntry: version: int content_hash: str in_flight: bool = False def _hash(value: bytes) -> str: return hashlib.sha256(value).hexdigest() class DivergenceDetector: def __init__(self, holds_exclusive_lease: bool): self._ledger: dict[str, LedgerEntry] = {} self._lease = holds_exclusive_lease def record_write(self, key: str, version: int, value: bytes) -> None: # Written before the store acknowledges the write to callers. self._ledger[key] = LedgerEntry(version, _hash(value), in_flight=True) def ack_write(self, key: str, version: int, value: bytes) -> None: self._ledger[key] = LedgerEntry(version, _hash(value), in_flight=False) def check_read(self, key: str, version: int, value: bytes) -> None: observed = _hash(value) entry = self._ledger.get(key) if entry is None: return # never written by us; not our boundary if entry.in_flight: return # unacknowledged write window; defer if observed == entry.content_hash: if version != entry.version: # benign rewrite of identical content self._ledger[key] = LedgerEntry(version, observed, in_flight=False) return if version == entry.version and observed != entry.content_hash: if self._lease: raise MemoryDivergenceError( f"content changed at fixed version {version} for key {key!r}; " f"expected {entry.content_hash[:12]}, got {observed[:12]}" ) return # indeterminate: no exclusive lease if version > entry.version: raise MemoryDivergenceError( f"version advanced {entry.version}->{version} with foreign content " f"for key {key!r}" ) def verify_forget(self, key: str, still_present: bool) -> None: if still_present: raise MemoryDivergenceError( f"forget of key {key!r} reported success but key still readable; " f"deletion failed under replication lag" ) ```