Skip to content

Retrieval

Memory retrieval has three tiers, picked at call time based on what the underlying SQLite build supports and what providers are available. The hybrid tier blends vector similarity with FTS5 BM25; the FTS5 tier uses BM25 alone; the LIKE tier is the floor.

The three tiers

TierMethodWhen
likeWHERE content LIKE %query% ORDER BY created_at DESCalways available
fts5memories_fts MATCH ? with BM25 ranking, Porter stemming, unicode61 tokenizerhasFts5 is true (SQLite was built with FTS5)
hybridvector similarity (cosine) + FTS5 fusionEmbeddingService has a working provider

MemoryActor exposes memory.searchTier so callers can probe which tier is active. Search degrades gracefully — a hybrid query with no embeddings falls through to FTS5; FTS5 unavailability falls through to LIKE.

FTS5 ranking

searchMemoriesRanked (AgentsStore.ts:237-249) builds a quoted FTS query from whitespace-tokenized words:

sql
SELECT m.*, memories_fts.rank
FROM memories_fts
JOIN memories m ON m.id = memories_fts.rowid
WHERE memories_fts MATCH ? AND m.mount = ?
ORDER BY memories_fts.rank
LIMIT ?

memories_fts.rank is BM25 (negative for better matches). The convenience helper bm25ToScore (MemoryActor.ts:25-29) maps it to a [0, 1) score for callers that want a normalized confidence.

Hybrid ranking

When embeddings are available, EmbeddingService participates:

  1. Embed the query.
  2. Pull candidate memories' embeddings (memory_embeddings).
  3. Compute cosine similarity (cosineSimilarity exported from MemoryActor.ts:67-77).
  4. Combine with FTS5 BM25 score (currently a simple weighted sum; the weights are not user-tunable).

Embeddings are cached in embedding_cache keyed by content hash + provider + model so the same text isn't re-embedded across sessions or providers.

Cascade

Cascade applies on top of any tier. searchMemoriesCascade (AgentsStore.ts:251-259) over-fetches by 8x then filters by memoryBelongsToCascade. The 8x heuristic is a working compromise — there is no FTS-aware cascade query yet.

What gets logged

Every cross-actor read is logged in memory_access_log (schema.sql:129-138):

(accessor_mount, target_mount, op, query, result_count, search_tier, ts)

memory.audit-trail returns this log filtered by target_mount.

The memory.consult op

MemoryActor.accepts['memory.consult'] is a higher-level op: ask the actor to answer a question from its own memory context. Internally it runs a memory.search and synthesizes an answer through inference. The current implementation is naive — it does not use a structured prompt template and quality is dependent on the underlying provider. Treat memory.consult as a convenience wrapper, not a production retrieval pipeline.

Embedding providers

EmbeddingService (EmbeddingService.ts) supports three sources with auto-fallback:

ProviderSourceWhen chosen
OpenAItext-embedding-*OPENAI_API_KEY set
Geminitext-embedding-*GEMINI_API_KEY set
Locala small ONNX model bundled with the packagealways available

Auto-fallback runs in that order. Once a provider is selected, embeddings are cached so subsequent calls do not re-evaluate eligibility.

See also

Source: projects/matrix-3/packages/agents/src/AgentsStore.ts:231-265 (search tiers), projects/matrix-3/packages/agents/src/MemoryActor.ts:24-77 (BM25→score, cosine), projects/matrix-3/packages/agents/schema.sql:39-105 (FTS5 setup, embedding cache).