ADR-032: Identity fields: deterministic dedup, detect-don’t-merge
Date: 2026-06-15 Status: Accepted (owner-delegated review, 2026-06-15) Author: Rômulo Febasi
Extends ADR-028 (open types) and ADR-022 (write gate). Companion to ADR-031/SYN-242 (contradiction → gate).
Context
mem0’s #1 unsolved bug is duplicate facts: “My name is X” then “My name is Y” stores both (exact-hash dedup only). In Synapse a re-capture of the same real-world thing under a different title produces a different slug (the id = slug = filename) and so a different entity: a silent duplicate the human gate can’t easily spot. SYN-242 catches structured contradictions (same subject+predicate, different object) but not “same thing, captured twice.” We need a deterministic way to recognise that two captures denote the same identity: without an opaque entity-resolution model and without breaking the pillars.
Decision
A custom type may declare identity fields in types.toml: the
fields whose combined values denote the same real-world thing:
[types.book]
indexed = ["isbn", "author"]
identity = ["isbn"] # two books with the same isbn are the same book
At propose-time, a capture_fact whose identity-field values match an
existing entity of the same kind (with a different id) is flagged as a
duplicate-identity collision and routed to the human gate: exactly
like a contradiction (SYN-242), surfaced with a contradicts/collision
signal so the agent can self-correct (re-capture as a x-supersedes update,
or drop the duplicate). Three hard constraints:
- Never auto-merge. Detect, don’t resolve: the human decides
(consistent with
core::contradiction; auto-merge is the <26%-correct stage HaluMem measures). - Never derive the id/slug from the identity hash. The id stays the slug = filename = human-editable, git-diffable, stable. The identity is a comparison fingerprint, not the key. This protects Markdown-as-truth + git-native.
identity ⊆ indexed. Every identity field is already a bound query key (ADR-023 invariant): the match is a parameterised lookup over the core columns +entity_fields, never interpolation.
Detection is a deterministic set-intersection of candidate ids, one identity
field at a time (bound values), bailing the moment the intersection is empty.
Opt-in per type (empty identity = no dedup). Fuzzy/MinHash/LSH matching is
deferred: single-user corpora don’t have the volume that makes blocking
pay, and fuzzy matching introduces false-merge risk.
Consequences
- Improves: the deterministic first tier of entity resolution that the
field’s best systems use; sharpens SYN-242 (a duplicate identity is a kind
of contradiction); keeps the gate’s incoming quality high (the gate’s value
is the scarce reviewer attention). Zero new deps; reuses
entity_fields. - Worsens: one more
types.tomlknob; the O(n) candidate scan per propose (fine at single-user scale; revisit only under a SYN-227 budget breach). Identity fields must beindexed, slightly constraining the declaration. - Open: cross-kind identity; the merge action (today it’s detect→gate→manual supersession): deferred until real demand. The merge-safe multi-author angle is a Commercial/team concern, not single-user.
- Update (SYN-254 expansion, 2026-07-06): identity matching now folds
case, accents (Unicode NFD + combining-mark strip), and whitespace before
comparison (
model::slug::normalize_for_match), so a re-capture differing only in those is detected: while punctuation is preserved (C++≠C#) to avoid over-merge. This is safe here precisely because the design is detect-don’t-merge: widening detection only routes more candidates to the human gate, never auto-merges. Fuzzy/similarity (MinHash/LSH/edit-distance) stays rejected: it is over-engineering at single-user scale and would trade the deterministic guarantee for a similarity threshold to tune.
Alternatives considered
- Derive the entity id from
hash(identity fields). Rejected: it makes the id unstable under any identity-field edit, fightinggit blame,syn at <ref>, human editability, and diffability. The hash is a comparison fingerprint, never the key. - Fuzzy/MinHash/LSH blocking. Over-engineering at single-user scale and a false-merge hazard; a flat exact-match scan over a few thousand candidates is correct and cheap.
- Auto-merge on collision. Violates the moat (the human gate) and is the exact stage the field gets wrong (<26%). Detect-don’t-merge.
- A full entity-resolution model. Opaque, non-deterministic, off-thesis.
Research grounding
mem0 #4896 (duplicate facts); HaluMem auto-update correctness <26%;
Zep/Graphiti + mem0 LLM reconciliation as the hard tier; classic
deterministic-first / blocking entity resolution. See
docs/researchs/2026-06-15-v0.11-issue-direction-study.md §3 (SYN-254).