Appearance
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
| Tier | Method | When |
|---|---|---|
like | WHERE content LIKE %query% ORDER BY created_at DESC | always available |
fts5 | memories_fts MATCH ? with BM25 ranking, Porter stemming, unicode61 tokenizer | hasFts5 is true (SQLite was built with FTS5) |
hybrid | vector similarity (cosine) + FTS5 fusion | EmbeddingService 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:
- Embed the query.
- Pull candidate memories' embeddings (
memory_embeddings). - Compute cosine similarity (
cosineSimilarityexported fromMemoryActor.ts:67-77). - 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:
| Provider | Source | When chosen |
|---|---|---|
| OpenAI | text-embedding-* | OPENAI_API_KEY set |
| Gemini | text-embedding-* | GEMINI_API_KEY set |
| Local | a small ONNX model bundled with the package | always 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).