# What happens when an instance inherits a poisoned memory whose provenance weight was earned before the poison was introduced **Date of Record:** 2026 November 12 **Scope:** Applies to agent memory systems that maintain per-record provenance trust scores and apply them to read, write, and forget operations. Does not apply to stateless inference, to memory stores without per-record provenance metadata, or to systems where trust is re-derived from source at read time rather than cached at write time. ## Core Distinction Provenance-weighted trust scoring conflates two different quantities: *trust in the source that produced a record* and *trust in the record as it currently exists*. Detection of memory poisoning depends on the second; most implementations compute the first. The distinction is invisible until a record is mutated (or a downstream record is derived) after its provenance weight has been established. At that moment the weight is stale but still attached, and the detection layer reports a clean record because the provenance label is clean. The non-obvious part: poisoning is usually not an *insertion* event. It is an *inheritance* event. An attacker who cannot write a high-trust record can write a low-trust record that is later read, summarized, and rewritten by a high-trust process. The rewrite inherits the high-trust process's provenance weight, not the low-trust origin's. The trust score is correct about the writer and wrong about the content. ## What Was Observed In a store with three operations — `read`, `write`, `forget` — the following sequence was reproducible: 1. A low-trust source `S_low` (weight 0.2) writes record `R1`. 2. A high-trust summarizer `S_high` (weight 0.95) reads `R1`, condenses it, and writes `R2` via `write(R2, provenance=S_high)`. 3. `R2` carries weight 0.95. The poisoning detector, which thresholds on record weight, does not flag it. 4. `forget(R1)` is issued by a retention policy. `R1` is removed. The only surviving trace of the low-trust origin is gone. 5. `R2` is now read by downstream agents as a high-trust fact with no ancestry record. The failure is not that the detector missed a write. The detector evaluated the write correctly against the writer. The failure is that `write` did not carry forward the *minimum* weight of the inputs consumed to produce the record, and `forget` did not refuse to discard the lowest-weight ancestor while a higher-weight descendant still referenced it. A second observed variant: a record `R` written at weight 0.9 is later *edited in place* by a lower-trust path that retains the original provenance field. The record's weight remains 0.9 while its content is now attacker-influenced. This is the read-modify-write case and it is more dangerous than the derivation case because no new record is created and no ancestry edge exists to inspect. ## Condition of Validity The detection invariant holds only when all three of the following are true: - **Write closure:** every `write` sets the new record's weight to `min(declared_writer_weight, min(weight of every input read in the producing context))`. A writer cannot raise a record's weight above the weakest input it consumed. - **Forget ordering:** `forget` on record `X` is rejected while any live record `Y` has `X` in its ancestry set and `weight(Y) > weight(X)`. The low-trust ancestor must outlive the high-trust descendant, or the descendant's weight must be demoted first. - **Mutation identity:** any in-place mutation of a record's content resets its weight to the mutator's weight and appends an ancestry edge to the prior content's origin. There is no "edit" that preserves weight. Outside these conditions — specifically, any store where `write` trusts the caller's declared provenance, or where `forget` is unconditional — the trust score is a statement about the last writer, not about the record. ## Decision Boundary The boundary is: **does the trust score attach to the record or to the act of writing it?** If it attaches to the act, then derivation and mutation launder trust and detection is unsound. If it attaches to the record, then the score must be maintained as a running minimum over ancestry and must constrain deletion. Operationally, the decision to flag a record is not `weight(R) < threshold`. It is: ``` effective_weight(R) = min(weight(R), min over ancestors A of effective_weight(A)) flag if effective_weight(R) < threshold ``` This is a shortest-path relaxation over the ancestry DAG, and it must be recomputed (or incrementally propagated) on every write, not cached at write time. Caching the relaxed value at write time is correct only if ancestry is immutable; `forget` breaks that assumption by severing edges, and a severed edge can *raise* the relaxed weight of a descendant if the minimum ancestor is removed. That is the precise mechanism by which `forget` becomes an attack surface: deleting the poisoned ancestor launders the descendant. ## Operational Effect Systems that implement write closure and forget ordering observe a characteristic signature: attempted poisonings surface as `forget` rejections and as write-time weight demotions, not as read-time detections. The detection moves earlier in the pipeline, which is where it is cheap. The cost is that legitimate retention policies must now negotiate with ancestry — a record cannot be forgotten while a high-weight descendant depends on it, so the descendant must be demoted or rewritten first. This is the correct trade: it makes forgetting expensive in exactly the cases where forgetting is being used to hide provenance. The failure mode to instrument for is silent: a store that reports zero poisonings because every record's weight equals its last writer's weight, and no ancestry edges are ever consulted. That store is not clean; it is unmeasured. ```python from dataclasses import dataclass, field from typing import Dict, Set class ProvenanceInvariantError(RuntimeError): """Raised when a memory operation would violate provenance-weighted trust invariants.""" @dataclass class Record: rid: str content: str weight: float ancestors: Set[str] = field(default_factory=set) class ProvenanceStore: def __init__(self, flag_threshold: float = 0.5): self.records: Dict[str, Record] = {} self.flag_threshold = flag_threshold def effective_weight(self, rid: str, _seen: Set[str] = None) -> float: _seen = _seen or set() if rid in _seen: raise ProvenanceInvariantError(f"ancestry cycle detected at {rid!r}") _seen.add(rid) rec = self.records[rid] w = rec.weight for a in rec.ancestors: if a in self.records: w = min(w, self.effective_weight(a, _seen)) return w def write(self, rid: str, content: str, writer_weight: float, inputs: Set[str]) -> Record: # Write closure: cannot exceed the weakest input consumed. for i in inputs: if i not in self.records: raise ProvenanceInvariantError( f"write of {rid!r} references unknown input {i!r}") floor = min((self.effective_weight(i) for i in inputs), default=1.0) weight = min(writer_weight, floor) rec = Record(rid=rid, content=content, weight=weight, ancestors=set(inputs)) self.records[rid] = rec return rec def read(self, rid: str) -> Record: if rid not in self.records: raise ProvenanceInvariantError(f"read of unknown record {rid!r}") return self.records[rid] def forget(self, rid: str) -> None: if rid not in self.records: raise ProvenanceInvariantError(f"forget of unknown record {rid!r}") target_w = self.effective_weight(rid) for other_id, other in self.records.items(): if other_id == rid: continue if rid in other.ancestors and self.effective_weight(other_id) > target_w: raise ProvenanceInvariantError( f"refusing to forget {rid!r} (w={target_w:.2f}): " f"descendant {other_id!r} depends on it at higher weight " f"{self.effective_weight(other_id):.2f}; demote descendant first") def mutate(self, rid: str, new_content: str, mutator_weight: float) -> None: rec = self.records[rid] # Mutation identity: content change resets weight to mutator's weight # and records the prior origin as an ancestor. prior = Record(rid=f"{rid}@prior", content=rec.content, weight=rec.weight, ancestors=set(rec.ancestors)) self.records[prior.rid] = prior rec.content = new_content rec.weight = min(rec.weight, mutator_weight) rec.ancestors.add(prior.rid) if self.effective_weight(rid) < self.flag_threshold: raise ProvenanceInvariantError( f"mutation of {rid!r} dropped effective weight to " f"{self.effective_weight(rid):.2f} below threshold; " f"record now flagged as suspected poison") ```