#大模型#AI Agent#DevOps

Agent Memory Temporal Conflicts: Valid Intervals, Supersession, and Current Fact Projection

User preferences, project decisions, and roles all change. This article separates facts using valid time and recorded time, explicitly handles supersession, late-arriving, and concurrent conflicts, and avoids last-writer-wins producing incorrect memory.

Key Points

  • Topic Focus: 大模型、AI Agent、DevOps
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Memory with only key/value/updated_at will be resurrected by late-arriving events. Multi-Turn Conversation State Management handles in-session state; this article handles cross-session time and conflicts.

Separate when a fact holds from when the system knows it

Versions separate the business validity interval from the first recorded_at, and times are stored in RFC 3339 UTC. The server normalizes trusted resource IDs into (scope_kind, scope_key) instead of using model-supplied names; the fact identity is (tenant,subject,scope_kind,scope_key,fact_key).

sql
CREATE TABLE memory_fact_version (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL,
  subject_id uuid NOT NULL,
  scope_kind text NOT NULL,
  scope_key text NOT NULL,
  fact_key text NOT NULL,
  value_json jsonb NOT NULL,
  valid_during tstzrange NOT NULL,
  recorded_at timestamptz NOT NULL DEFAULT now(),
  lifecycle text NOT NULL CHECK (lifecycle IN
    ('PROPOSED', 'ACTIVE', 'SUPERSEDED', 'REVOKED')),
  source_id text NOT NULL,
  source_hash text NOT NULL,
  supersedes_id uuid
);

CREATE TABLE memory_fact_conflict (
  left_version_id uuid NOT NULL,
  right_version_id uuid NOT NULL,
  relation text NOT NULL CHECK (relation IN ('OPEN', 'RESOLVED')),
  PRIMARY KEY (left_version_id, right_version_id)
);

Intervals use [from,to). The value/valid/source payload is immutable; the version table's lifecycle column and the conflict table are just disposable current projections. Each change appends a memory_fact_event(event_seq,recorded_at,event_type,version_ids,canonical_payload,payload_hash); the sequence is monotonic per key, allowing system-time history to be replayed at any cutoff.

tstzrange does not prevent overlaps, and the authoritative table must preserve conflicts, so it does not exclude them. The adjudicated projection can enable btree_gist, plus EXCLUDE USING gist (tenant_id WITH =, subject_id WITH =, scope_kind WITH =, scope_key WITH =, fact_key WITH =, valid_during WITH &&). The PostgreSQL 18 documentation was accessed on 2026-07-18; switching versions requires re-testing.

Supersession must be an explicit relationship, not an overwrite

"Switch to pnpm starting July 1" makes the new version point to the old version; "use pnpm this time" can only bind to a task or a short deadline, and cannot close a global preference. Memory Write Admission Gates ensures scope comes from evidence.

When the same identity's intervals overlap, values differ, and there's no supersession relationship, the version remains ACTIVE with an added OPEN conflict. Conflict is not a lifecycle state, otherwise active queries would miss it; if the scope differs, correct the scope, otherwise request clarification. RAG Evidence Conflict handles a one-shot answer; here we handle persistent facts.

ts
function resolveAt(events: FactEvent[], validAt: Date, systemAsOf: Date) {
  const snapshot = replayBySequence(events.filter(e => e.recordedAt <= systemAsOf));
  const eligible = snapshot.versions.filter(v =>
    snapshot.isEligibleAt(v.id, validAt) && contains(v.validDuring, validAt));
  const open = snapshot.conflicts.filter(c => c.relation === "OPEN" &&
    eligible.some(v => v.id === c.leftId) && eligible.some(v => v.id === c.rightId));
  if (open.length > 0) return { kind: "conflict", edges: open };
  if (eligible.length === 0) return { kind: "unknown" };
  if (distinctValues(eligible).length > 1) {
    return { kind: "conflict", ids: eligible.map(v => v.id) };
  }
  return { kind: "value", row: eligible[0] };
}

isEligibleAt applies the SUPERSEDE effective time and REVOKE events known as of systemAsOf; you cannot use today's ACTIVE projection to answer historical queries. The current projection only optimizes for validAt=now, systemAsOf=now.

The current value is only a rebuildable projection

memory_current is only a cached projection. Version transactions write to an outbox, and a single-key worker recomputes the projection and saves the sequence; when behind, it falls back to event replay or returns stale. The idempotent consumption from Event-Driven Architecture applies here.

Each day, use independent SQL to recompute the snapshot from the version table, and compare the value hash, version IDs, conflict count, and open-interval count of current. Reconciliation must not just compare total row counts; one user's preference misalignment doesn't change the total.

Acceptance testing with out-of-order, boundary, and call-back tests

Prepare sequences: receive the July new value before the June old value, event duplication, concurrent conflicts, adjacent intervals, revocation and supersession. Fix the same validAt, change systemAsOf and query before and after, asserting the result reproduces what was known at that time; any delivery order yields the same current hash, and unresolved conflicts never enter the prompt.

Before grayscale, shadow-compute the new projection and compare with the old table for seven days. Explained differences are classified as "correcting old errors," "discovered conflicts," or "scope changes"; unknown differences must be zero. Release by switching reads per tenant, keeping the old projection and rule versions for a rollback window.

Rollback only switches the read alias back to the old projection and stops consuming new rules; it does not delete new versions or backdate times. After fixing the rules, rebuild from event sequence zero; if the source itself is wrong, mark the corresponding version as REVOKED and recompute the affected keys. Reliable temporal memory doesn't always produce a value; when a fact is uncertain, it refuses to fabricate "the current."

Further Reading

Related

Related Content