Skip to main content

πŸ” Recall Memories

recall searches all memories and returns the best matches. The two engines search in different places.

Search runs fully locally against the index (flushed and pending); no network round trips:

const matches = await memory.recall('how do I escalate for ACME?', {
tags: ['customer'], // optional: only memories carrying at least one of these tags
limit: 5, // default 5
})

console.log(matches[0])
// {
// id: '80cc2055-…',
// content: 'Customer ACME is on the enterprise plan; …',
// score: 0.62, // final ranking score
// semanticScore: 0.71, // cosine similarity (0..1, when embeddings active)
// keywordScore: 0.41, // keyword/tag overlap
// tags: ['customer', 'escalation'],
// cid: 'baf…',
// gatewayUrl: 'https://gateway…/ipfs/baf…'
// }

How scoring works​

With the local embedder (the hosted default; in the SDK pass embedder: await createEmbedder('local'), since BatchedEngine defaults to keyword-only):

score = 0.7 Γ— cosine(query, memory) + 0.3 Γ— min(1, keywordScore)
  • Semantic component - query and memories are embedded locally with all-MiniLM-L6-v2 (in-process; the ~25 MB model downloads once, and no content leaves the machine). This is what lets "which hosting platform is used for releases?" find "deploys to Vercel on the main branch" with zero shared keywords. Vectors carry their model id (embeddingModel); recall only compares same-model vectors, so switching models safely falls back to keyword until backfilled.
  • Keyword component - matching content words dampened by memory length (hits / √words), plus +0.5 for each tag that appears in the query.
  • Ties break by recency.

In the SDK, omit embedder or pass null for word-match only (on the hosted server, set MEMORY_EMBEDDER=keyword) - zero deps, fully offline. If the embedding model cannot load (e.g. offline on first use), recall degrades gracefully to pure keyword scoring - status() reports which mode is active.

No relevance floor

With the local embedder, every memory that has a vector is a candidate, so recall() always returns up to limit results - even when nothing is related to the query. (Keyword-only mode does drop zero-score matches.) There is no minScore option; filter on score yourself:

const MIN_SCORE = 0.3 // tune on your own data
const matches = (await memory.recall(query, { limit: 10 })).filter((m) => m.score >= MIN_SCORE)

Search runs in the relayer over the encrypted blobs; results are enriched with your local tags/CIDs:

const matches = await memory.recall('how do I escalate for ACME?', {
tags: ['customer'], // applied locally after the relayer returns hits
limit: 5, // default 5
maxDistance: 1.2, // optional: drop matches with distance >= this value
})

console.log(matches[0])
// {
// blobId: '…', content: '…', distance: 0.38, score: 0.62,
// id, tags, agent, createdAt, cid, pinned, gatewayUrl, walrusUrl,
// indexed: true // false = relayer knows it, this machine has no local record yet
// }

score is 1 βˆ’ distance clamped to 0..1. There is no local embedder to configure - memwal needs none (remote).

Other read operations​

await memory.list({ limit: 20 })   // newest first (memwal also accepts { tags })
await memory.get(idOrCid) // full record by id or CID (memwal: also Walrus blob id)
await memory.status() // engine, namespace, counts, index state

Engine extras:

// batched
await storage.getBlobIds(cid) // Walrus-native blob IDs (lh-ipfs-walrus only, else [])

// memwal
await memory.verify(ref) // integrity: gateway byte-compare if pinned, else local re-hash
await memory.blobIds(ref) // { memwalBlobId, recordBlobIds }
await memory.analyze(text, { occurredAt }) // extract discrete facts into separate memories
await memory.restore({ maxBlobs }) // ask relayer to rebuild missing vector-index entries
await memory.repinPending() // retry failed IPFS pins (see pendingPins in status)

Every flushed batch blob / pinned memwal record is also readable by anyone directly from the gateway:

curl https://gateway-walrus.lighthouse.storage/ipfs/<CID>
curl https://gateway.lighthouse.storage/ipfs/<CID>