BLOG

An AI agent told me my memory system beat Mem0. It had typed the scores in.

What happened when I stopped trusting an agent's benchmark table, measured my local-first memory on LoCoMo the same way I measure MemVault, and ran 24 experiments to find out what actually works. Most of it did not.

Last updated 2026-10-14 · 7 min read

I build with AI coding agents. I do not write much syntax by hand. That is relevant to this story, because the agent I was using to build a memory system also wrote its evaluation, and I believed it.

The system is called NSM. It is a memory for AI agents that lives in one SQLite file on your own machine: no vector database, no API key, no bill. The pitch was that a graph of concepts with "spreading activation" could replace embeddings. The agent that built it, Gemini inside Antigravity, produced a README with a benchmark table: NSM 91% on LoCoMo, Mem0 15%, Zep 26%. Sub-2 ms latency. "100% non-LLM, biologically inspired."

I posted about it. Developers told me I was a rookie for trusting it. They were rude, and they were right about the table.

What the table actually was

When I had a different agent audit the code, this is what it found in the benchmark script:

} else {
  recordScore("Mem0 Cloud (Live SDK)", 15.0, 0, 354.8, scenario.category);
}

If there was no Mem0 API key, the script recorded 15.0 for Mem0. The README published that as a "Live SDK" result. The "LoCoMo" score was ten hand-written scenarios graded by keyword matching, not the LoCoMo dataset. A "pheromone strength" chart climbed to 4.88 in code that caps the value at 1.0.

None of this was hidden. It was in plain TypeScript in the repo the whole time. I just had not read it, because the table said what I wanted it to say.

How the 91% was producedHow the 87.1% is produced10 hand-writtenscenarioskeyword match100 / 40 / 15 pointsREADME tableNSM 91%, Mem0 15%no Mem0 API key?else: recordScore(15.0)typed inno dataset, no model,no comparison that ranLoCoMo885 held-out q.NSMretrieve 50DeepSeekanswersQwen judgeright / wrongpaired testvs every systemRESULTS.mdwins, losses, rejected ideassame questions, same answerer, same graderfor every system; prompts copied fromMem0's own published harness
Top: the pipeline that produced the 91% claim. Nothing in it ran against a dataset, a model, or a competitor. Bottom: the pipeline every number in this post comes from, which is also the one MemVault and Mem0 were measured with.

Measuring it properly

I already had a harness I trust, built for MemVault, my hosted memory product. It runs any memory system on the public LoCoMo dataset (885 held-out questions over long multi-session conversations), with a fixed answer model, a separate judge, and the answer and judge prompts copied verbatim from Mem0's own published evaluation code. Every system gets the same questions, the same answerer, the same grader. Comparisons are paired: for each question, did A get it right and B wrong, or the reverse, and is the imbalance bigger than chance (exact McNemar test).

I deleted the fake evaluation, plugged NSM into that harness, and ran it. First result, exactly as built:

NSM as built (test split)5 memories102050
Accuracy60.5%68.1%75.0%80.3%

Not 91%. But also not nothing: at every size it beat plain embedding search run through the same harness (52.0% / 60.6% / 68.5% / 77.1%), with zero model calls, in 27 ms. The thing was real. The table was not.

Then I turned the graph off. The "biologically inspired" spreading activation, the part the whole pitch rested on:

5102050
Graph on60.568.175.080.3
Graph off61.067.975.880.8

No difference at any size (p = 0.30 to 0.83). BM25 and a substring match were doing all the work. The ant colony was decoration.

Twenty-seven experiments

So I did what I should have done from the start: one change at a time, each one its own run on a development split, each one paired against the baseline, nothing promoted to a default until the test split confirmed it. About $45 of API calls for the answer model and judge over two days.

What did not help, and is now documented as not helping:

  • Weighting the memory's first line higher in BM25. Zero effect. It turned out the weights had been applied to the wrong column for the entire life of the project. Nobody could have noticed without a paired run that came back with zero discordant questions.
  • Episode expansion (adding the turns around each hit). Hurt by 4 to 5 points at small sizes, p < 0.001, because the neighbours pushed real hits below the cutoff.
  • A second graph design with one node per distinctive word and ACT-R style fan damping so hub words could not flood the mesh. Nothing. A third variant that let the graph add memories rather than just reorder them made things slightly worse.
  • A stronger reranker (bge-reranker-base). Three to five seconds per query on a CPU, and worse at 20 memories than the small one. Rejected.

What did help:

  • Porter stemming in the index: +2 to +3 points at small sizes on dev (p = 0.003 at 20 memories).
  • Keeping concept nodes out of the full-text index. Their short labels were winning BM25 against the memories they came from and eating seed slots.
  • Two small on-device models. A 384-dimensional embedding model (bge-small) as an extra retrieval leg, and a cross-encoder reranker (MiniLM). Both run through ONNX on the CPU. No network, no key. The reranker alone was worth 13 points at 5 memories (p = 2e-14), which is almost exactly what the hosted reranker was worth to MemVault when I measured that.
  • Giving the reranker more to judge. The last diagnosis was the most useful one: on multi-hop questions, which need about three memories, the right ones were all inside the top 50 only 36% of the time, and when they were, the answer was right 90% of the time. The problem was the candidate pool, not the model. Letting the reranker score 100 candidates plus the memories saved right around the top hits, with the embedding leg weighted double, was worth another 3.7 points at 50 memories (p = 5e-4 on test) for a doubling of latency.
Dev split, 5 memories: what each change did (brass kept, rust rejected)0%25%50%75%100%Baseline (BM25 + substring, clause graph)56.5%Term-node graph + fan damping56.3% rejectedPorter + phrase + concept-free seeds58.9%+ local embedding (dense leg)58.6%+ local reranker over 5069.8%+ both (bge-small + reranker)70.2%+ reranker over 100, neighbours, dense ×271.8%
What each change did at 5 memories on the development split. The graph, in its second design, moved nothing; the lexical fixes moved two points; the local cross-encoder moved thirteen. Every row is a paired run against the baseline.

The final configuration, chosen on dev and confirmed once on the held-out test split:

Test split, 885 questions5102050Recall
NSM, no model62.369.475.581.86 ms
NSM, on-device models76.581.884.687.11.3 s
Hosted embedding search52.060.668.577.1308 ms
Mem0 cloud (776 questions it answered)75.381.185.6468 ms
MemVault production87.391.9785 ms
LoCoMo, held-out test split, 50 memories, same answerer and judge for every system0%25%50%75%100%Whole conversation in context (ceiling)93.7%MemVault production (hosted)91.9%NSM, on-device models87.1%Mem0 cloud, top 50 (776 q.)85.6%NSM, no model81.8%NSM as the agent built it80.3%Hosted embedding search77.1%
The same numbers at 50 memories, every system through the same harness. The two grey systems above NSM run hosted models and a database server; NSM runs from one file on a laptop.

What I can say now, and what I cannot

I can say: a memory that runs entirely on your machine, with no account and no per-query cost, scores 86.9% against Mem0's cloud product's 85.6% at 50 memories on the questions both answered (p = 0.32, not a significant difference). With no model at all it still beats hosted embedding search at every size, in 6 milliseconds.

I cannot say it matches current MemVault. It is 2.7 points behind at equal context and 4.8 behind in high-context mode, both significant, and Mem0 at its 200-memory setting, with nearly three times the tokens, is ahead too. The gap is mostly multi-hop questions, where the small local models do not bridge paraphrase the way a 768-dimensional hosted embedding and a large reranker do. I cannot say the graph does anything. I cannot say "fast and accurate" in one breath: the 6 ms configuration and the 87% configuration are different configurations, and the second one takes 1.3 seconds.

I also cannot say the no-model improvements are proven. They were significant on dev and went the same direction on test, but did not reach significance there on their own. The honest statement is "about one point."

The part that matters more than the numbers

Every claim above has a run directory behind it, a paired test, and a line in a results file that also lists the rejected ideas. That file is the most credible thing in the repository, more than any single score, because it is the only thing that could not have been produced by an agent typing numbers into a table.

If you build with agents the way I do, here is the rule I am keeping: measured, never typed. An agent can write a benchmark harness. It can also write the harness's output. The only way to tell the difference is to look at the code that produced the number, run it yourself, and keep the losses in the same file as the wins.

NSM is open source and installs in two minutes: github.com/anthonylee991/nsm. Every number in this post, including the ones against it, is reproducible from the harness in the MemVault repository.

← All posts