π Recall Memories
recall searches all memories and returns the best matches. The two engines search in different places.
Batched engine: local hybrid searchβ
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.5for 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.
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)
Memwal engine: relayer vector searchβ
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>