The problem
Extraction echoes whatever spelling the text used. Over a few thousand memories the graph ends up holdingClaude Opus 4.8 and Opus 4.8, Postgres and PostgreSQL, Claude Code
and claude-code as separate entities.
That is not just untidy. Recall’s entity arm anchors a query to at
most 10 entities, so every duplicate spelling spends one of those slots naming a thing
another slot already names. Retrieval gets worse as the graph grows.
Name-key resolution catches only the cheapest case (@memloom/core and memloom/core).
Everything else needs a decision.
What a fold is
A fold picks one entity as the canonical and moves the other into it:- every mention edge and typed relationship repoints to the canonical,
- the absorbed spelling is recorded as an alias, with the original row’s id and vector,
- a merge record stores exactly which edges moved and which were deactivated.
Opus 4.8 lands on the canonical instead of
minting the row again. Without it a fold quietly undoes itself as soon as new content
arrives, which no amount of re-running the backfill would reveal.
How candidate pairs are found
Candidate generation is lexical and deterministic. Measured against a real 1794-entity store, the duplicates a graph accumulates are spelling variants, not synonyms, so an embedding is used only to confirm a pair the rules already found. It never pairs two entities on its own. That keeps precision high and costs nothing per pair. Entities are bucketed by their words and by the first characters of their normalized key, so only plausible pairs are ever compared. Each pair then gets one of three verdicts.The rules that say no
These run first, because the pairs that look most similar are often the ones that must stay apart.The one rule that folds
Two names whose normalized keys are identical after casefolding and dropping every non-alphanumeric character.Claude Code, claude-code and ClaudeCode all collapse to
claudecode. This is the only case applied without asking, with two escapes:
- keys shorter than 3 characters ask instead (
C#andCcollide), - a punctuation-prefixed name asks instead (
.nextis a build directory,Nextis a framework).
The rules that ask
People are the one exception to the two-word requirement. A human’s name is written at
several lengths and means one human, so
Ada pairs with Ada Lovelace. Both
sides must be typed person, and it still only asks: John against both John Smith and
John Doe is a real question.
Three of these rules exist because of what a real store did to earlier versions. Naive word
containment produced 1129 questions, because a one-word name like
memloom sits inside
memloom serve, MEMLOOM_HOME and @memloom/cli. Requiring two contiguous words, refusing
punctuation-prefixed names, and scaling edit tolerance with name length took that to 241.Running it
deferred exists so that “nothing queued” never hides “there was more to ask about”. The
queue is capped at 50 pending questions so entity folds cannot bury the memory conflicts the
surface exists for; the lowest-impact questions wait, ranked by the weaker side’s mention
count, and a later pass picks them up as answers come in.
Uncertain folds go to the conflicts queue
An uncertain fold is the same shape of question as a contradiction between two memories: something is inconsistent and only you can settle it. So it lands in the same place, with the same reversibility. See Conflicts.On an entity row,
keep_both means “settled, do not ask again”. It is written both by you
choosing that the two are distinct and by the resolver settling a question that became moot
because both sides were already folded together. Do not read the decision log as a record of
human judgment about distinctness.Recall follows folds
The fold deletes the absorbed row, so without help a query spelled the old way would stop anchoring on anything, and folding would make retrieval worse. The absorbed vector is not gone, though: it moved to the alias row. The entity arm anchors on entity vectors and alias vectors together, mapping every alias hit to its canonical. Canonical and alias are deduplicated before the limit is applied, since two spellings of one name have close vectors and would otherwise spend two anchor slots naming one entity. The net effect on a real store: 38 correct automatic folds freed 37 entities from competing for the anchor budget, and a query using a folded spelling now reaches everything the canonical is attached to.Everything is reversible
POST /memory/entities/merges/{id}/revert restores the absorbed entity with its original
id, type, vector and creation time, repoints exactly the edges the merge moved, reactivates
exactly the edges it deactivated, and removes the alias.
The original id matters: deactivated edges still point at it, so a restored row must be the
same row, not a new one wearing the same name.
Asking the graph who is connected to what
Once identity is settled, the graph can answer questions about connections rather than content:- Stated relationships are typed predicate edges extraction asserted (
works_on,uses,part_of), with their direction and the extractor’s confidence. - Co-mention is entities named by the same sources, with the count. Weaker evidence, so it never outranks a stated link.
related_entities MCP tool.
What it deliberately will not do
A nickname that shares no letters with the name, likeBob against Robert, will
never be proposed. That is not a lexical judgement and no rule here pretends otherwise.
Declaring it yourself works and needs nothing new: POST /memory/entities/{id}/merge folds
any two entities regardless of spelling, records the same reversible merge, and every future
mention of the alias resolves to the canonical. The missing piece is only a channel that
proposes such a pair, which today is you.