Engineering notes

Naming Things Twice

Under-merge and lookups return zero from a store full of the answer; over-merge and two near-namesakes fuse into someone who never existed. Finding the line is a judgment call — so the interesting engineering is everything wrapped around a judgment that will sometimes be wrong.


18 July 2026·9 min read

A store of structured facts — who works where, what was decided when, which company the dispute is with — earns its keep by being exact. Ask it about a named subject and it answers with rows, not guesses. But exactness has a supply chain: the answer is only as good as the key it was filed under, and the keys come from people, who do not spell things the same way twice.

Every fact written into our graph mints its key from whatever name string it was handed. Write about someone by their first name today and their full name on Friday and the store now holds two subjects. Nobody decided that; nobody would. It is what an exact-keyed store does under ordinary human naming variance, one write at a time, and the failure it produces is the quiet kind: a lookup that returns nothing, from a store that is full of the answer. We measured the worst case in an earlier entry — one person split across four keys, and a company whose most natural query normalised to a key that did not exist while more than twenty facts sat under three adjacent spellings.

The tempting fix is a string rule: lowercase everything, strip the punctuation, collapse the variants. It cannot work, and the reason it cannot work is the interesting part. Some things that share a name really are different things. In our own store, the company, the long-running dispute about the company, and the company's registered legal entity are three genuinely distinct subjects wearing one name root — and a normaliser aggressive enough to heal the person split across four spellings will also fuse those three into one, which does not merely lose precision, it manufactures false facts. Merge two near-namesakes and the store now asserts things about a subject that never existed.

So the squeeze is this: under-merge and the store keeps returning zero for questions it can answer; over-merge and it starts answering questions wrongly, with confidence, forever. There is no threshold between those two that a string rule can find, because the difference between a variant spelling and a different thing is not in the strings. It is a judgment about the world.

A judgment pass with machinery around it

So entity resolution falls to something that can exercise judgment, wrapped in machinery that assumes the judgment will sometimes be wrong.

A model reads the full list of subjects — key, display name, how many facts each holds — and proposes clusters: groups of keys that are clearly the same real-world thing, each with a chosen canonical key and display name. The prompt is conservative to the point of timidity. Do not merge distinct-but-similar things; leave anything that looks like a file path alone; skip singletons; when in doubt, propose nothing. An empty answer is a valid answer and a common one.

Then the machinery takes over, and none of it trusts the proposal. The store — a SQLite database — is backed up before a single row moves, and the backup stays on disk even when everything succeeds. The merge runs as one transaction — every fact's subject and object references rewritten from variant keys to the canonical one, orphaned variants removed, exact-duplicate facts collapsed — and an integrity check on the far side aborts the whole thing, backup intact, if the store comes out anything less than clean. Re-running after a clean pass finds nothing to merge and changes nothing. And the run returns a full audit: what merged into what, how many rows moved, where the backup lives, plus the model's own reasoning — returned even when it proposed no merges, because why nothing merged is information too.

The division of labour is the transferable part. The model contributes the one thing rules cannot — knowing that a person's four spellings are one subject and a company's three near-namesakes are three — and contributes nothing else. Whether the operation is safe never depends on the model being right; it depends on backups, transactions and integrity checks, which do not have judgment and do not need any.

The assumption that did not survive reading the source

The filed-notes side of memory has the same disease in a different organ. Notes are filed into named areas and topics, the names are stored verbatim, and over months the map grows near-duplicates — the same area under a lowercase-and-underscores spelling and a Title Case one, side by side. Our working assumption was that this fragmentation was hurting search, and the fix was urgent for that reason.

Then we read the engine's source before building, and the assumption was wrong in a useful way. The area and topic labels are stored as metadata on each note; what gets embedded for semantic search is the note's content, only ever the content. The recall pipeline searches across everything without filtering by area at all. So duplicate spellings were doing no damage whatsoever to retrieval — a search finds the note identically under either spelling of its area, because the spelling is not in the vector.

What the duplicates actually damage is comprehension. The map the user opens shows two areas where there is one place; the cheap orientation glance the agent takes at the start of retrieval reads the same fragmented list. Real costs — but presentation costs, not recall costs, and the honest re-scoping changed both the priority and the design. The backfill that merges variant spellings ships as map maintenance, not as a search fix, and it rewrites labels only: nothing is re-embedded, so the vectors — and therefore every ranking — are byte-identical before and after. A change that provably cannot move search results is a change you can run without a search evaluation.

And because a backfill alone would re-fragment with the next write, the durable half of the fix sits at the boundary we own. We do not own the memory engine; we own the wrapper every write passes through. So the wrapper now snaps an incoming area or topic name onto an existing spelling when the two differ only by case or separators — choosing the most-populated spelling as canonical, and letting the write through untouched if the taxonomy read fails, because a filing nicety must never block a memory being saved. The map converges toward one spelling per place without the engine changing at all.

Spelling is not meaning

One more place the naming problem surfaces, and it forced a choice that looks backwards until it doesn't. The graph screen needed search — a person hunting for an entity should not need to know which of four spellings the store filed it under, or spell an accented surname correctly. The obvious modern answer is embeddings; everything else in this system searches by meaning.

We used edit distance instead — a plain string-similarity matcher from Python's standard library, combined with substring and per-token scoring. Because a proper-noun search is not a meaning problem. The user typing a misspelled first name does not want entities that are semantically like that person; they want the entity whose spelling is nearest. Vector similarity is the wrong instrument for that: it clusters by what things are, and two unrelated people's names can sit closer in meaning-space than one person's name and its own typo. Measured on our store, the misspelling of a first name scores 0.77 against its target and a four-letter fragment of another scores 0.89 against its own — comfortably above the 0.55 floor that keeps weak cross-matches out.

The asymmetry: only the human got this. The screen can now find a fragmented or misspelled entity that the agent's own graph lookup — still exact-keyed — continues to miss. Until that closes, one operational rule stands everywhere the graph is consulted: a keyed lookup that returns nothing proves nothing. A miss means unknown, never absent, and any code or prose that treats graph silence as evidence of absence is wrong by construction.

Reorganising a memory while it is being used

The spelling merge turned out to be the small case of something bigger. Left to accrete, the map does not just duplicate names — it sprawls. Every agent files each note into whatever area felt right in the moment, and months of that produces a hoard of half-named corners only a machine could love. People do not hold their lives that way; they hold them in a dozen understood places.

So the merge grew up into a full re-taxonomy pass — the product calls it dreaming, and the name is doing real work: a quiet pass that gathers the scattered notes and re-settles them into life-areas a person recognises, previewed as a diff before anything moves, reversible after. The interesting engineering is not the reorganisation. It is the contract that makes reorganisation safe to offer at all, in a product whose headline promise is recall:

The pass is metadata-only by construction, so however much it rearranges, recall is provably unchanged. Not tested-and-found-unchanged — provably, from the property established above: search never reads the labels the pass rewrites, and the pass never touches the content search reads. The two operations are disjoint at the storage layer, so no volume of re-filing can move a single ranking. Alongside that, the pass never enters a private agent's sealed area — a fenced agent's notes stay exactly where its permissions put them — and anything that would touch content (merging near-duplicate notes, summarising) is explicitly deferred behind a recall evaluation gate it has to pass first.

That is the shape worth taking away, because it applies to any long-lived store that needs maintenance while in service: divide every maintenance operation into the part that is provably inert to reads and the part that is not, ship the first freely, and gate the second on evidence. A reorganisation you can prove safe needs a preview and a backup. A reorganisation you can only test safe needs an evaluation harness and a much better reason to exist.


Postscript, August 2026. The judgment-plus-machinery shape held, and the dream pass now runs as one combined cycle — merge the spellings, then re-settle the areas — available to every user, with the model's reasoning trace kept admin-side. What has deliberately not moved: the agent's graph lookup is still exact-keyed, the bridge that would let semantic search hand the graph its keys is still unbuilt, and so "a miss proves nothing" remains a standing rule rather than a closed chapter. It resurfaced within weeks in a different subsystem — an agent asserting something was absent on evidence that could not prove absence — and that incident got its own machinery, and will get its own entry.

One letter a month, when there is something worth saying.

What we shipped, what we learned, and the occasional thing that did not work. No drip sequence, no launch countdowns.

Unsubscribe any time. Or take the RSS feed instead.