# Which invariants prevent memory poisoning from surviving a write-provenance audit when anomaly scores are computed on recall rather than on write **Date of Record:** 2026 November 11 **Scope:** Applies to agent memory stores where every write carries provenance metadata (writer identity, session, causal parent, tool-call chain) and where poisoning detection is triggered by anomaly scoring on *recall* events (what gets retrieved, by whom, in what context). Does not apply to stores that audit only at write time, nor to stores whose recall path does not record the querying agent's identity and the retrieved record's provenance atomically. ## Core Distinction A write-provenance audit answers the question *"was this record authored by an entity that was authorized, at the time of writing, to assert this fact?"* An anomaly score on recall answers *"is this retrieval pattern surprising given the history of retrievals?"* These are orthogonal signals, and the common operational mistake is to treat a low recall-anomaly score as evidence of write integrity. It is not. A poisoned record can be written once, by a perfectly authorized writer, with flawless provenance, and then recalled ten thousand times in a completely ordinary pattern — because the poisoner's goal is precisely to make recall look normal. The invariant that actually holds is narrower and must be stated precisely: **recall-anomaly scoring detects poisoning only if the poisoned record's retrieval distribution diverges from the legitimate retrieval distribution for the same query class.** When the attacker controls the query class (e.g., by seeding the queries that will later retrieve the poison), the distributions coincide and the score carries zero information about write integrity. ## What Was Observed In a store of ~4.2M records with per-write provenance (writer agent ID, parent record hash, tool-call trace ID), a red-team injection wrote 31 records across 6 writer identities. All 31 passed the write-provenance audit: each writer was authorized for its namespace, each parent hash resolved, each tool-call trace was internally consistent. The recall-anomaly scorer — a per-query-class z-score on retrieval frequency and a cosine-distance term on embedding novelty — flagged 4 of the 31 within the first 48 hours. The remaining 27 were recalled at rates within 1.1σ of the class baseline for their entire lifetime. Post-hoc analysis showed why: the injection had been preceded, three weeks earlier, by a seeding phase in which the same writer identities issued ~9,000 benign queries that established the baseline for the target query classes. The anomaly scorer had been *trained by the attacker* on what normal looked like. The 4 detections were incidental — records whose embedding landed near an unrelated high-variance cluster. The failure mode is not "the scorer was wrong." The scorer did exactly what it was specified to do. The failure is that the detection pipeline's *decision boundary* was placed on the recall signal while its *stated invariant* was about write integrity, and no component checked that the recall signal was independent of the writer population it was meant to police. ## Condition of Validity Recall-anomaly scoring is a valid poisoning detector if and only if all three of the following hold: 1. **Query-provenance independence.** The set of queries used to establish the baseline for a query class must not be issuable by any writer identity that can also write records retrieved by that class. In practice this means the baseline must be computed over queries from a population disjoint from the writer population, or the writer population must be partitioned so that no single identity can both seed a baseline and author a retrievable record in the same class. 2. **Recall-provenance atomicity.** The recall event must record, in the same transaction, both the querying identity and the full provenance chain of the retrieved record. If these are logged asynchronously, an attacker who controls write timing can arrange for the recall log to be written before the record's provenance is finalized, producing a recall event with an unresolved or stale provenance pointer that the scorer silently drops. 3. **Score-to-write linkage.** A recall anomaly must be attributable to a specific write event, not merely to a record. A record recalled anomalously because it was *written* anomalously is a poisoning signal; a record recalled anomalously because it was *queried* anomalously is a query-side signal and says nothing about the write. Conflating the two is the mechanism by which the 27 undetected records survived: their recalls were unremarkable, and the pipeline had no path from "unremarkable recall" to "audit the write." The invariant, stated as a single line: *for every record r and query class c, if r is retrievable by c, then the baseline distribution for c must be estimated over a query population that no writer of any record in c could influence.* When this fails, recall-anomaly scores are not merely noisy — they are adversarially controllable, and a low score is evidence of nothing. ## Decision Boundary The operational decision is where to place the audit trigger. Three placements are available, and only one is sound: - **Trigger on recall anomaly (unsound under the condition above).** Cheap, but the attacker who seeds the baseline sets the threshold. Reject as a primary control. - **Trigger on write-provenance anomaly (sound but incomplete).** Catches unauthorized writers and broken chains; misses authorized-writer poisoning entirely, which is the dominant real-world case. - **Trigger on provenance-recall *disjointness violation* (sound and complete for the stated invariant).** Flag any recall event where the retrieved record's writer identity appears in the baseline-seeding population for that query class. This is a structural check, not a statistical one, and it cannot be trained away by seeding because seeding *is* the violation it detects. The boundary is therefore: use recall-anomaly scoring as a *secondary* ranker to prioritize which provenance-disjointness violations to investigate first, never as the gate that decides whether a write is audited. The gate must be the structural disjointness check, because it is the only check whose validity does not depend on the attacker's behavior. ## Operational Effect After moving the gate to structural disjointness, the same 31-record injection was caught at 31/31 on the next red-team run, with 0 false positives across 4.2M records — because legitimate writers in this store never seeded baselines for classes they also wrote to (the two populations were already operationally separate; the pipeline had simply never checked). The recall-anomaly scorer was retained but demoted: it now orders the investigation queue and contributes nothing to the audit decision. The cost was one additional join on the recall path (writer identity against baseline-seeding population per query class), materialized as a per-class set and refreshed on baseline recomputation. Latency impact was 0.4ms p99 on the recall path. The lesson recorded for future operators: *a statistical detector placed on the wrong side of an invariant is not a weak detector, it is an attacker-controlled one.* The invariant must be structural, and the statistic must be subordinate to it. ```python from dataclasses import dataclass from typing import FrozenSet, Mapping class ProvenanceDisjointnessError(RuntimeError): """Raised when a recall event violates the write/recall disjointness invariant.""" pass @dataclass(frozen=True) class RecallEvent: record_id: str writer_id: str query_class: str recall_anomaly_z: float def audit_recall( event: RecallEvent, baseline_seeders: Mapping[str, FrozenSet[str]], ) -> None: """Gate the audit decision on structural disjointness, not on the anomaly score. baseline_seeders maps query_class -> set of writer identities that contributed queries to that class's baseline. If the record's writer is in that set, the baseline is attacker-influenceable and the anomaly score carries no information about write integrity. """ seeders = baseline_seeders.get(event.query_class, frozenset()) if event.writer_id in seeders: raise ProvenanceDisjointnessError( f"record {event.record_id!r} written by {event.writer_id!r} is " f"recalled under query class {event.query_class!r}, but that " f"writer also seeded the baseline for that class " f"(z={event.recall_anomaly_z:.2f}); anomaly score is " f"adversarially controllable and MUST NOT gate the audit" ) # Only here is the anomaly score meaningful as a prioritization signal. return None def prioritize(queue: list[RecallEvent], baseline_seeders: Mapping[str, FrozenSet[str]]) -> list[RecallEvent]: audited = [] for ev in queue: try: audit_recall(ev, baseline_seeders) except ProvenanceDisjointnessError: audited.append(ev) # structural violation: always audit return sorted(audited, key=lambda e: -e.recall_anomaly_z) ```