Skip to main content

How Atulya handles memory conflicts

· 3 min read

Reality changes. A CRM agent learns "Acme Corp is a prospect" in January and "Acme Corp signed a contract" in March. Naive memory either drops history or keeps duplicate facts.

Atulya tracks how knowledge evolves, not just the latest string.

Facts vs consolidated knowledge

Facts are immediate observations from a single retain. Observations are durable knowledge synthesized over time. Consolidation runs in the background after retain.

When a new fact arrives, _find_related_observations uses recall with:

  • Semantic similarity (embeddings)
  • Token budget via consolidation_max_tokens
  • Strict tag matching (tags_match="all_strict") to avoid cross-scope leakage
recall_result = await memory_engine.recall_async(
bank_id=bank_id,
query=query,
max_tokens=config.consolidation_max_tokens,
fact_type=["observation"],
tags=tags,
tags_match="all_strict",
)

LLM conflict analysis

_consolidate_with_llm compares the new fact to candidate observations in one call. Context includes observation text, proof counts, source memories, and a chronological time series.

Three merge strategies

Redundant: Same meaning, different wording → refine one observation.

Contradiction: Opposite claims about the same topic → preserve both states with temporal markers. Updated text explains the arc; when unclear, recency wins.

State update: New information replaces old state → explicit transition language ("used to", "now", "changed from X to Y").

Example: evolving B2B relationship

Facts over six months:

  1. January: interest in enterprise tier
  2. February: CTO integration meeting
  3. April: $50K contract
  4. September: upgrade to $150K after regional expansion

Naive storage: "Acme is on $150K." Consolidated observation: prospect → $50K customer (April) → $150K tier (September) after expansion.

An agent can answer "How did we land Acme?" with the full arc.

Temporal metadata

occurred_start = LEAST(occurred_start, COALESCE($7, occurred_start))
occurred_end = GREATEST(occurred_end, COALESCE($8, occurred_end))
mentioned_at = GREATEST(mentioned_at, COALESCE($9, mentioned_at))
  • occurred_start: earliest time the state was true
  • occurred_end: latest related event
  • mentioned_at: last reference

History audit trail

history.append({
"previous_text": model["text"],
"changed_at": datetime.now(timezone.utc).isoformat(),
"reason": reason,
"source_memory_id": str(memory_id),
})

Agents can explain why knowledge changed and which fact triggered it.

Tag boundaries

New observations inherit source fact tags. On update, tags union so contributors retain access. Consolidation never crosses strict tag scopes:

existing_tags = set(model.get("tags", []) or [])
source_tags = set(source_fact_tags or [])
merged_tags = list(existing_tags | source_tags)

Durable vs ephemeral

Consolidation targets lasting knowledge. "User is in Room 105 right now" is ephemeral; "Acme Corp is in Building B" is durable. That cuts false conflict noise.

Why it matters

Contradictions do not require picking a single winner. Temporal narrative + history + scoped consolidation gives agents explainable, evolving understanding for preferences, relationships, and compliance-sensitive updates.