Conflicting and Contradictory Memories
How the plugin handles facts that contradict or update existing ones: classify-before-write (ADD/UPDATE/DELETE/NOOP), supersession, and bi-temporal queries.
Overview
When new information contradicts or replaces old information, the system can:
- Classify before storing — Ask an LLM whether the new fact is truly new (ADD), updates an existing fact (UPDATE), retracts one (DELETE), or is redundant (NOOP).
- Supersede — Mark the old fact as superseded and link the new one (valid_from / valid_until, supersedes_id).
- Query by time — Exclude superseded facts by default, or ask “what did we know as of date X?” (point-in-time).
No automatic merging of text: UPDATE creates a new fact row that supersedes the old; the old row stays for history but is excluded from normal search.
Classify-before-write
When store.classifyBeforeWrite is true, every store (from memory_store, auto-capture, or batch) runs a classification step before writing:
- Find similar facts — The new text is embedded and compared to existing facts (LanceDB + SQLite). A small set of the most similar facts is collected (with preference for same entity/key when available).
- LLM decision — The new fact plus those existing facts are sent to a cheap LLM. It must respond in a fixed format: ADD, UPDATE, DELETE, or NOOP, plus an optional target fact ID and a short reason.
- Apply:
- ADD — No conflict; store the new fact as usual.
- UPDATE — The new fact replaces an existing one. The existing fact is marked superseded; the new fact is stored with
supersedes_idpointing to it. - DELETE — The user is retracting an existing fact. That fact is marked superseded; the new “fact” is not stored (it’s a retraction).
- NOOP — Already captured; do not store.
Classification uses a dedicated prompt (e.g. memory-classify) and a configurable model (default gpt-4o-mini). On parse or API errors, the code falls back to ADD so storage still succeeds.
Where it runs
- memory_store tool (when classifyBeforeWrite is enabled).
- Auto-capture — When a captured message is about to be stored, classification runs; UPDATE/DELETE/NOOP are applied the same way.
- CLI —
hybrid-mem store(when classifyBeforeWrite is enabled). - extract-daily — Daily scan / batch extraction uses classification when classifyBeforeWrite is enabled.
Performance warning: batch imports and classify-before-write
Warning: Classification is implemented one new fact at a time: each candidate store runs
classifyMemoryOperation, which issues at least one chat completion to decide ADD / UPDATE / DELETE / NOOP. On batch or scripted paths (extract-daily, repeated CLIstore, or any loop that stores many facts), enablingclassifyBeforeWritetherefore causes O(n) LLM calls for n facts—higher latency, cost, and risk of provider rate limits. Interactive auto-capture only considers a small capped number of messages per turn, so the blast radius there is limited.
Mitigations you can use today:
- Turn off
store.classifyBeforeWritefor a one-off bulk import, then re-enable afterward. - Prefer smaller extract windows or fewer facts per run when classification must stay on.
- Track issue #862 for future batched classification (multiple facts per LLM request).
Config
| Option | Default | Description |
|---|---|---|
store.classifyBeforeWrite | false | Enable ADD/UPDATE/DELETE/NOOP classification before every store. |
store.classifyModel | gpt-4o-mini | Model used for the classification call. |
Supersession
Supersession is how we record “this fact replaces that one” without deleting history.
Database fields
- On the old (superseded) fact:
superseded_at(timestamp),superseded_by(ID of the new fact),valid_until(end of validity). - On the new fact:
supersedes_id(ID of the old fact),valid_from(start of validity).
valid_fromis usually set fromsource_dateorcreated_at.
What happens on UPDATE
- The old fact is updated:
superseded_at = now,superseded_by = newFactId,valid_until = now. - The new fact is inserted with
supersedes_id = oldFactIdandvalid_fromset. - Search and lookup exclude rows where
superseded_at IS NOT NULLunless you passincludeSuperseded: trueor a point-in-timeasOf.
So by default, “current” recall never sees superseded facts; they remain in the DB for auditing and point-in-time queries.
Manual supersession
The memory_store tool and the CLI accept an optional supersedes target:
- memory_store: Pass the
supersedesparameter (a fact ID). The specified fact is marked as superseded (same fields as above), and the new fact is stored withsupersedes_idset to that ID. - CLI:
hybrid-mem store --text "..." --supersedes <fact-id>does the same for scripted or manual updates.
Use this when you know explicitly which fact is being replaced (e.g. after reviewing duplicates or after a user correction).
Bi-temporal and point-in-time queries
Every fact has valid_from and valid_until (Unix seconds). Together with supersession this gives bi-temporal behaviour:
- valid_from — When this version of the fact became valid (often creation or source date).
- valid_until — When it was superseded (null if still current).
Point-in-time search
You can ask “what did we know as of date X?” so that superseded facts are still visible for that time:
openclaw hybrid-mem search "database" --as-of 2026-01-15
openclaw hybrid-mem lookup "user" --key "theme" --as-of 2026-01-15
The search and lookup add:
valid_from <= @asOf AND (valid_until IS NULL OR valid_until > @asOf)
so you see only facts that were valid at that moment.
- CLI:
hybrid-mem searchandhybrid-mem lookupsupport--as-of <date>(ISO date or epoch seconds) and--include-superseded. - memory_recall tool: Parameters
asOf(ISO date or epoch) andincludeSuperseded(default false). WhenasOfis set, only facts valid at that time are returned (including LanceDB results filtered by SQLite validity). - CLI store:
hybrid-mem store --text "..." --supersedes <fact-id>marks the given fact as superseded and stores the new fact withsupersedes_idandvalid_from.
Relation to deduplication
Conflicting-memory handling is the most advanced layer of deduplication:
- Exact text — Skip if the same text already exists.
- Fuzzy hash — Skip if normalized text matches (when
store.fuzzyDedupeis on). - Vector similarity — Skip adding to LanceDB if a very similar vector exists (SQLite fact still stored).
- Classify-before-write — LLM decides ADD/UPDATE/DELETE/NOOP; UPDATE/DELETE implement contradiction resolution.
See DEEP-DIVE.md for the full deduplication section.
Summary
| Mechanism | Purpose |
|---|---|
| Classify-before-write | Decide per store whether to ADD, UPDATE, DELETE, or NOOP using an LLM. |
| Supersession | Mark old fact as superseded; link new fact via supersedes_id; set valid_until / valid_from. |
| Default search | Exclude superseded facts so recall is “current” only. |
| Point-in-time | Use --as-of (or API equivalent) to query “what was true at date X?”. |
| Manual supersedes | Pass supersedes: factId in memory_store when you know which fact is replaced. |
Related docs
- DEEP-DIVE.md — Supersession and deduplication sections
- FEATURES.md — Classification pipeline and config
- CONFIGURATION.md —
store.classifyBeforeWriteandstore.classifyModel - GRAPH-MEMORY.md — SUPERSEDES link type and graph traversal