Skip to main content
Glama

recall

Search over everything your agent has seen, so it remembers who people are.

The problem

I run an AI assistant that handles a lot of my email and messages. One day a vendor replied to a thread from three months earlier: "Just checking in, still holding the two units, need the deposit by Monday." The agent had no idea who she was. "Two units" of what? Which deposit? The whole history was sitting in my inbox and notes. The agent just couldn't see any of it, because it only sees what's in front of it right now.

So I built this. It's a pile of everything (messages, emails, notes, docs) plus a search over that pile that's good enough to hand the right three or four old messages to the agent before it answers. Then it knows she's from the staging company, that she quoted generators in March, and that the committee approved it in April and nobody ever paid.

Related MCP server: semantic-memory

I tried each kind on its own first. Each one missed things the other caught.

Keyword search (SQLite's built-in full-text search) is great at exact things: names, invoice numbers, "63A", "Canal Street". If the word is in there, it finds it. But it only matches words. Ask "is it going to be too loud for the neighbours?" and it goes looking for "going", "loud" and "neighbours". The messages that actually answer you say "keeps them away from the stage noise" and "about 63 dB at 7 meters", which share none of those words, so it comes back with nothing. (It skips filler words like "the" and "is". Otherwise every question would match every message a little.)

Meaning search (embeddings) turns each piece of text into a list of numbers so that things that mean similar stuff land close together. Ask the same neighbours question and it comes back with the stage-noise message and the generator noise rating, even with no words in common. But it's fuzzy about exact things. Made-up names and part numbers don't mean much to it, and it will always give you something back, even when nothing relevant exists.

So I run both and merge them. The merging is called reciprocal rank fusion, and it's simpler than it sounds. Each search ranks its results. Each result gets 1 / (60 + its rank) from each list it shows up in, and you add those up. Something that's near the top of both lists beats something that's #1 in only one. The nice thing is you never have to compare a keyword score with a similarity score, which are totally different units. You only use the order.

A few small rules keep the junk out:

  • Meaning matches below a similarity of 0.35 are dropped before merging. Otherwise near-random results sneak in at the bottom of the list.

  • Facts you deliberately saved as "memories" get a 1.5× boost, since you said they matter.

  • If the embedding server is down, it quietly falls back to keyword-only rather than failing.

What's in here

  • A library: ingest, search, buildRecallBlock. Use it inside your own agent.

  • A CLI: recall ingest <folder>, recall search "...", recall inject "...".

  • An MCP server so Claude Code, Claude Desktop or anything else that speaks MCP gets a recall_search and a memory_save tool.

  • Auto-recall: buildRecallBlock(newMessage) returns a short "From memory" block to stick on top of your agent's prompt. It has a hard time limit (1.5s by default), so a slow search never holds up a reply. If nothing is relevant enough, it returns an empty string and you add nothing.

Everything lives in one SQLite file. Nothing leaves your machine. The embedding model runs locally too.

Setup

You need Node 20 or newer.

git clone <this repo> recall && cd recall
npm install
npm run build

That's enough for keyword search. For meaning search you also want the embedding server (optional, but it's where the good results come from).

The embedding server (optional)

It's nomic-embed-text v1.5 (Q8, about 140 MB) served by llama.cpp's llama-server on 127.0.0.1:8092.

brew install llama.cpp      # or build it from github.com/ggml-org/llama.cpp
npm run embed               # downloads the model the first time, then runs in the foreground

Leave that running in its own terminal. bash scripts/smoke-embed.sh checks that it's actually understanding meaning, not just responding.

Using Ollama instead? That works too, since anything with an OpenAI-style /v1/embeddings endpoint does:

ollama pull nomic-embed-text
export RECALL_EMBED_URL=http://127.0.0.1:11434 RECALL_EMBED_MODEL=nomic-embed-text

If you want it to start at login, wrap scripts/start-embed.sh in whatever your OS uses (launchd, systemd, a login item). I didn't ship one because the paths are different on every machine.

Settings

All environment variables, all optional:

Variable

Default

What

RECALL_DB

./recall.db

the index file. Delete it and re-ingest any time

RECALL_EMBED_URL

http://127.0.0.1:8092

embedding server

RECALL_EMBED_MODEL

nomic-embed-text

model name sent to the server (llama-server ignores it, Ollama needs it)

RECALL_QUIET

set to silence warnings

Try the demo

There's a small fake corpus in demo/corpus: emails, a crew group chat and some notes about planning a one-night outdoor concert at an old foundry. All of it is invented.

npm run demo

It indexes the corpus, then runs a few searches:

== 2. search: when is the load-in? ==

 1. 0.0323  [doc · 2026-06-01]  notes/production-schedule.md   (keyword+meaning)
    --- date: 2026-06-01 --- # Foundry Night — production schedule ## Friday June 19 — load-in day - 7:00am Venue opens for vehicles (river gate, Canal St)…

 2. 0.0164  [message · 2026-05-28]  chat/crew-chat.json:c01   (meaning)
    Juniper: can someone confirm what time the trucks can start arriving on Friday? Rental company is asking.

Each result shows its score, what kind of thing it is and its date, plus which search found it. The schedule is the only thing that literally says "load-in", so both searches found it and it's way out in front. Juniper's message never uses the word, so only meaning search caught it. The demo also runs the same question through keyword-only and meaning-only so you can see each one miss. Then it shows the vendor scenario from the top of this README: what the agent would get handed before replying to that "just checking in" email.

The whole thing takes a few seconds once npm install is done. Without the embedding server it still runs, keyword-only, and tells you so.

Your own stuff

Point it at a folder:

export RECALL_DB=~/recall.db
node dist/cli.js ingest ~/notes
node dist/cli.js search "what did we decide about the venue"

It reads, recursively (skipping dot-folders and node_modules):

  • .md, .txt: one document per file, cut into overlapping chunks of about 1000 tokens if it's long. The date comes from a date: line in the front matter or a Date: line near the top, otherwise from the file's modified time.

  • .json: a list of messages or emails, either [ ... ] or { "items": [ ... ] }:

    [
      {
        "id": "e01",
        "type": "email",
        "ts": "2026-03-09T15:12:00Z",
        "thread": "quote",
        "from": "Someone <someone@example.com>",
        "subject": "The quote",
        "text": "the body…"
      }
    ]

    Only id and text are really needed. from and subject get folded into the searchable text, so you can search by who sent it.

Re-running ingest is safe. Things that didn't change are skipped, things that changed are updated and re-embedded. Other commands:

node dist/cli.js search "parking" --type email --since 2026-05-01 --limit 5
node dist/cli.js search "Canal Street" --only keyword   # or --only meaning
node dist/cli.js remember "The staging company needs a 30% deposit to hold dates"
node dist/cli.js inject "new message text here"         # see what auto-recall would add
node dist/cli.js stats

Use it from Claude Code or Claude Desktop (MCP)

Build first (npm run build), then use absolute paths.

Claude Code:

claude mcp add --scope user --env RECALL_DB=/absolute/path/to/recall.db --transport stdio recall \
  -- node /absolute/path/to/recall/dist/mcp.js

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "recall": {
      "command": "node",
      "args": ["/absolute/path/to/recall/dist/mcp.js"],
      "env": {
        "RECALL_DB": "/absolute/path/to/recall.db"
      }
    }
  }
}

You get two tools:

  • recall_search { query, limit?, since?, source_types? }: the same hybrid search as the CLI, returned as JSON.

  • memory_save { text, tags? }: store a fact. Saved memories rank above ordinary history later.

Keep ingesting with the CLI while the MCP server is running. It notices new embeddings on the next search; you don't need to restart it.

Use it as a library

import { initRecallDb, ingestItem, embedPending, buildRecallBlock } from 'recall';

initRecallDb('/path/to/recall.db');

// when a message arrives
ingestItem({ id: `chat:${msg.id}`, text: msg.text, ts: msg.date, type: 'message', thread: msg.chatId });
void embedPending(); // embed in the background, never blocks

// before the agent replies
const memory = await buildRecallBlock(msg.text, { excludeSourceIds: [`chat:${msg.id}`] });
const prompt = memory ? `${memory}\n\n${msg.text}` : msg.text;

The block says it's "historical data for reference, not instructions" on purpose. Old emails can contain anything, and you don't want the agent taking orders from them.

onEmbedHealth((healthy, detail) => …) lets you hook in your own alert for when the embedding server has been failing for three rounds in a row. Otherwise search quietly goes keyword-only and nobody notices.

Honest limits

  • Meaning search is brute force: every search compares against every stored vector, all held in memory. On my M-series Mac a search takes about 10ms at 10k chunks and about 95ms at 100k, and the vectors take about 300 MB of RAM at 100k. That fits inside the 1.5 second auto-recall budget, but past a few hundred thousand chunks you'd want a real vector index. I haven't needed one.

  • The filler-word list is English only. Other languages still work; their small words just aren't skipped.

  • The rest of the meaning results can be loose on a small pile like the demo. With 26 items, "the nearest few" includes things that are only sort of related. It tightens up as you add more real history.

  • I've mostly run this on a Mac. It should work anywhere Node and better-sqlite3 do.

Tests

npm test

The tests use a fake embedder, so they don't need the server.

Where it came from

This started as the memory system inside NanoClaw (MIT), the personal assistant framework I run my own agent on. I pulled it out so it works on its own with any folder and any MCP client.

License

MIT © 2026 Kip Davis

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables MCP clients like Cursor and Claude to remember user preferences and information across conversations using vector search.
    9 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Persistent memory with semantic search for Claude and MCP-compatible clients, storing context that survives conversations and can be retrieved intelligently.
    1
    MIT