# When must an agent refuse to resolve a contradiction between two memory records? **Date of Record:** 2026 November 6 **Scope:** Applies to long-horizon agent sessions that persist memory across turns or restarts and later reconcile conflicting records (facts, tool outputs, user statements) about the same subject. Does not apply to single-turn stateless inference, nor to contradictions that are purely presentational (formatting variance in otherwise identical values). ## Core Distinction The non-obvious boundary is between **contradiction resolution** and **provenance collapse**. Resolution is the legitimate act of choosing a newer or higher-trust record when the two records describe the same subject under the same world-state assumptions. Provenance collapse is the failure mode where an agent resolves a contradiction by overwriting or discarding a record without preserving (a) the source of each claim, (b) the observation time, and (c) the world-state epoch under which each was valid. The invariant: **a contradiction may only be resolved when both records are anchored to the same world-state epoch and both carry intact provenance.** If either record's provenance is missing, or if the two records were observed across an epoch boundary (e.g., a service was redeployed, a user changed their configuration, a file was rewritten by another process), the correct action is not to pick a winner but to mark the pair as *unresolved* and re-observe. Overwriting across an epoch boundary silently destroys the only evidence that the world changed — and that evidence is usually what the next turn needs. ## What Was Observed In a multi-day session maintaining a service-configuration memory, the agent recorded `retry_timeout = 30s` at epoch 4 from a config file read. At epoch 7, after a deploy, a tool output reported `retry_timeout = 5s`. The agent resolved the contradiction in favor of the newer value, wrote `retry_timeout = 5s` into memory, and deleted the epoch-4 record as "superseded." Two turns later the user asked why timeouts had changed. The agent could not answer: the epoch-4 record that would have shown the delta was gone, and the epoch-7 record carried no epoch tag, so the agent could not even prove the change was deploy-related rather than a transient misread. The memory was internally consistent and externally useless. The second observation is subtler: contradictions between records that share an epoch but differ in *source class* (user assertion vs. tool output vs. prior agent inference) should not be resolved by recency alone. A user statement at turn 12 does not automatically outrank a tool output at turn 11 when the tool output is a direct observation and the user statement is a recollection. Recency is a tiebreaker, not a trust ordering. ## Condition of Validity Resolution is valid only when all of the following hold: 1. Both records carry a non-null `source`, `observed_at`, and `epoch` field. 2. `record_a.epoch == record_b.epoch`. 3. The subject identity is confirmed equal (not merely string-similar) — aliasing must be resolved before comparison. 4. The chosen record's source class is at least as authoritative as the discarded one for this predicate type, or the discard is explicitly logged as a recency-based override. If any condition fails, the pair must be quarantined as an unresolved contradiction and re-observed against the live world before any write. This is the decision boundary that separates safe reconciliation from provenance collapse. ## Decision Boundary - **Same epoch, intact provenance, equal subject:** resolve; keep both records linked by a `supersedes` edge rather than deleting the loser. Deletion is never required for consistency. - **Cross-epoch:** do not resolve. Emit an epoch-change event, retain both records with their epoch tags, and schedule re-observation. The contradiction *is* the signal. - **Missing provenance on either side:** treat as unresolved regardless of recency. A record with no source cannot win a contradiction; it can only be re-derived. - **Source-class conflict within an epoch:** prefer direct observation over recollection, log the override, and retain the loser as a linked record. ## Operational Effect Implementing this boundary converts a silent overwrite into an explicit, auditable reconciliation event. Memory grows slightly (superseded records are retained as linked edges), but the agent retains the ability to answer "what changed and when," which is precisely the question that long-horizon sessions are asked. The failure mode it prevents — a memory that is confident, consistent, and unable to explain itself — is the dominant cause of provenance collapse in persistent agents. The enforcement below rejects resolution attempts that violate the boundary and forces the caller into the quarantine path. ```python from dataclasses import dataclass, field from typing import Optional class ProvenanceCollapseError(RuntimeError): """Raised when a contradiction resolution would destroy provenance.""" pass @dataclass class MemoryRecord: subject: str value: str source: Optional[str] observed_at: Optional[int] epoch: Optional[int] source_class: str = "inference" # "observation" | "user" | "inference" supersedes: list = field(default_factory=list) _SOURCE_RANK = {"observation": 3, "user": 2, "inference": 1} def resolve_contradiction(a: MemoryRecord, b: MemoryRecord, recency_override: bool = False) -> MemoryRecord: if a.subject != b.subject: raise ProvenanceCollapseError(f"subject mismatch: {a.subject!r} != {b.subject!r}") for rec in (a, b): if rec.source is None or rec.observed_at is None or rec.epoch is None: raise ProvenanceCollapseError( f"record for {rec.subject!r} lacks provenance " f"(source={rec.source!r}, observed_at={rec.observed_at!r}, epoch={rec.epoch!r}); " f"quarantine and re-observe" ) if a.epoch != b.epoch: raise ProvenanceCollapseError( f"cross-epoch contradiction for {a.subject!r}: epoch {a.epoch} vs {b.epoch}; " f"emit epoch-change event and re-observe, do not resolve" ) if a.value == b.value: return a rank_a, rank_b = _SOURCE_RANK.get(a.source_class, 0), _SOURCE_RANK.get(b.source_class, 0) if rank_a == rank_b or recency_override: winner, loser = (a, b) if a.observed_at >= b.observed_at else (b, a) else: winner, loser = (a, b) if rank_a > rank_b else (b, a) winner.supersedes = list(winner.supersedes) + [loser] return winner def reconcile(records: list) -> list: resolved, quarantined = [], [] for i in range(len(records)): for j in range(i + 1, len(records)): try: resolved.append(resolve_contradiction(records[i], records[j])) except ProvenanceCollapseError: quarantined.append((records[i], records[j])) if quarantined: raise ProvenanceCollapseError( f"{len(quarantined)} unresolved contradiction(s) require re-observation before memory write" ) return resolved ```