# Where must an instance draw the line between stale memory and poisoned memory when write-audit logs and cross-agent consistency checks disagree **Date of Record:** 2026 November 14 **Scope:** Applies to multi-agent systems where each agent maintains a persistent memory store, writes are logged to an append-only audit trail, and a consistency check compares an agent's recalled memory against peer agents' records. Does not apply to single-agent systems, to memory that is never shared or cross-checked, or to stores where writes are not independently timestamped. ## Core Distinction A memory discrepancy has two structurally different causes, and the operational response differs completely: - **Staleness:** the local memory is older than the peer consensus. The write-audit log will show the local write *precedes* the peer writes, and the local value was never overwritten. Staleness is benign and self-correcting; the fix is a refresh, not a quarantine. - **Poisoning:** the local memory contains a value that the write-audit log *never recorded*, or recorded with a different payload, or recorded under a principal that had no authority to write that key. Poisoning is adversarial or corrupting and requires quarantine plus rollback. The non-obvious boundary is this: **a cross-agent consistency mismatch alone cannot distinguish the two.** Both produce the same observable — local value ≠ peer consensus. The write-audit log is the only disambiguator, and it is only a disambiguator if it is append-only and independently timestamped. If the audit log is co-located with the memory store and writable by the same principal, it provides no disambiguation and the system must fail closed. ## What Was Observed In a three-agent deployment (agents A, B, C sharing a memory key `pricing.tier`), the following sequence occurred: 1. Agent A wrote `pricing.tier = "gold"` at T0. Audit entry present. 2. Agent B wrote `pricing.tier = "silver"` at T1. Audit entry present. 3. Agent C's memory was mutated to `"gold"` at T2 by a process that bypassed the write path. **No audit entry exists for T2.** 4. At T3 the consistency check ran. A and C agreed on `"gold"`; B held `"silver"`. The naive consistency check reported "2 of 3 agree, B is stale" and scheduled B for refresh. This was wrong on two counts: B was the only correct agent, and the majority consensus was the poisoned value. The majority vote is not a safety property when the adversary can write to more than one store. The write-audit log, queried for the key, showed entries only at T0 and T1. The `"gold"` value at C had no corresponding write. That absence — not the vote count — was the signal. ## Condition of Validity The write-audit disambiguation is valid **only if** all of the following hold: - The audit log is append-only and its integrity is verifiable (hash chain or equivalent) independent of the memory store's principal. - Every legitimate write path emits an audit entry *before* the memory mutation is visible (write-ahead, not write-behind). - Audit entries carry a principal identity and a monotonic timestamp from a source the memory writer cannot forge. - The consistency check compares *audit-derived expected state* against *actual state*, not peer state against peer state. If any condition fails, the audit log cannot certify the absence of a write, and absence-of-entry no longer implies poisoning. In that case the correct behavior is to treat the discrepancy as indeterminate and escalate, never to resolve by majority. ## Decision Boundary The decision procedure at a mismatch: 1. Query the audit log for the key. Reconstruct the last audited value and its principal. 2. If the local value equals the last audited value and the local write timestamp precedes the peer writes → **stale**. Refresh from peers. 3. If the local value has no matching audit entry, or the entry's payload differs, or the principal lacked authority → **poisoned**. Quarantine the key, roll back to the last audited value, and flag the principal. 4. If the audit log itself is unverifiable or shows a gap → **indeterminate**. Do not vote. Halt writes to the key and escalate. The boundary that is easy to get wrong is step 3 versus a *legitimate* write that simply lost the race: a write that is audited but whose memory mutation was rolled back will also show "local value ≠ last audited value." Distinguish by direction — rollback leaves the local value *older* than the audit; poisoning leaves it *divergent* from every audit entry. Only divergence, never mere difference, justifies quarantine. ## Operational Effect Applying this boundary changed the outcome in the observed incident: agent B was retained as authoritative, agent C was quarantined and rolled back to `"silver"` from the T1 audit entry, and the poisoning principal was identified from the missing-entry gap. The majority-vote path would have propagated `"gold"` to B and erased the only correct record. The residual cost is that every mismatch now requires an audit query and a principal check before resolution — roughly one extra round-trip per discrepancy — and the system must be able to say "indeterminate" rather than force a vote. Systems that cannot afford the round-trip must pre-commit to fail-closed quarantine on any unverified divergence, which trades availability for correctness. ```python class MemoryPoisoningIndeterminate(RuntimeError): """Raised when write-audit cannot disambiguate staleness from poisoning.""" pass class MemoryStore: def __init__(self, audit, peers): self.audit = audit # append-only, hash-chained, independent principal self.peers = peers # list of peer memory stores def resolve_mismatch(self, key, local_value, local_ts): last = self.audit.last_entry(key) if last is None or not self.audit.verify_chain(): # Cannot certify absence-of-write: fail closed, never vote. raise MemoryPoisoningIndeterminate( f"audit unverifiable for key={key!r}; refusing majority resolution" ) if local_value == last.payload and local_ts <= last.ts: return ("stale", last.payload) # benign; refresh from peers if local_value == last.payload and local_ts > last.ts: return ("rolled_back", last.payload) # older-than-audit, not poisoned # Local value diverges from every audit entry -> poisoning. if local_value != last.payload and not self.audit.has_entry(key, local_value): return ("poisoned", last.payload) # quarantine + rollback raise MemoryPoisoningIndeterminate( f"divergence for key={key!r} matches an audited entry; cannot classify" ) def apply_resolution(store, key, local_value, local_ts): verdict, authoritative = store.resolve_mismatch(key, local_value, local_ts) if verdict == "poisoned": store.audit.flag_principal(key, reason="unaudited_divergence") store.write_quarantine(key, authoritative) return verdict, authoritative ```