Embedding-Based Memory Recall — Implementation Plan
Replaces LIKE keyword search with semantic vector search for memory recall, using sqlite-vec and OpenAI embeddings with keyword fallback.
What this file does
Replaces LIKE keyword search with semantic vector search for memory recall, using sqlite-vec and OpenAI embeddings with keyword fallback.
When to use it
- Adding semantic search to an existing SQLite-backed memory system
- Replacing keyword-based recall with vector embeddings while keeping fallback
- Integrating sqlite-vec into a Node.js project using better-sqlite3
- Implementing background backfill for existing records when adding embeddings
Assumes this stack
Embedding-Based Memory Recall — Implementation Plan
Replace LIKE keyword search with semantic vector search for MemoryFS memory
recall. Uses @dao-xyz/sqlite3-vec for vector storage and OpenAI embeddings API
(configurable model) for embedding generation. Falls back to keyword search when
no OPENAI_API_KEY is set.
Architecture Overview
Write path (memorize / write):
content → EmbeddingClient.embed() → Float32Array
↓
ItemsStore.insert(item) ──────→ EmbeddingStore.upsert(itemId, vector)
Query path (recall / query):
user message → EmbeddingClient.embed() → Float32Array
↓
EmbeddingStore.findSimilar(vector, limit) → [{itemId, distance}]
↓
ItemsStore.getByIds(itemIds) → MemoryEntry[] (ranked by distance × salience)
Fallback: When OPENAI_API_KEY is absent, degrade to existing LIKE search.
History assembly order remains:
[memory recall] → [summaries] → [recent turns] → [current message]
Step 1: Add @dao-xyz/sqlite3-vec dependency
File: package.json
npm install @dao-xyz/sqlite3-vec
This brings sqlite-vec (the C extension) and a Node.js wrapper that auto-loads the vec extension into better-sqlite3.
Step 2: Create EmbeddingClient utility
New file: src/utils/embedding-client.ts
A standalone, stateless client for generating text embeddings. Wraps the OpenAI
SDK's embeddings.create() endpoint. Not an LLM provider — this is a utility.
interface EmbeddingClientConfig {
model: string; // e.g. 'text-embedding-3-small'
dimensions: number; // e.g. 1536
apiKey?: string; // defaults to OPENAI_API_KEY env var
baseUrl?: string; // defaults to https://api.openai.com/v1
}
interface EmbeddingClient {
embed(texts: string[]): Promise<Float32Array[]>;
readonly dimensions: number;
readonly available: boolean; // false when no API key
}
function createEmbeddingClient(config: EmbeddingClientConfig): EmbeddingClient
Key behaviors:
- Returns
available: falsewhen no API key set (no throw — graceful degradation) - Returns Float32Array for direct sqlite-vec consumption
- Uses
openaiSDK already in dependencies — no new HTTP client - Logs via
getLogger().child({ component: 'embedding-client' })
New test file: tests/utils/embedding-client.test.ts
- Test graceful degradation when no API key
- Test Float32Array output format
- Mock OpenAI SDK for unit tests
Step 3: Create EmbeddingStore for sqlite-vec operations
New file: src/providers/memory/memoryfs/embedding-store.ts
A vector store backed by sqlite-vec's vec0 virtual table. Separate from
ItemsStore — uses @dao-xyz/sqlite3-vec's createDatabase() for automatic
extension loading.
class EmbeddingStore {
constructor(dbPath: string, dimensions: number);
upsert(itemId: string, embedding: Float32Array): void;
findSimilar(query: Float32Array, limit: number, scope?: string): Array<{
itemId: string;
distance: number;
}>;
delete(itemId: string): void;
hasEmbedding(itemId: string): boolean;
listUnembedded(limit: number): string[]; // for backfill
close(): void;
}
Schema:
-- vec0 virtual table for vector similarity search
CREATE VIRTUAL TABLE IF NOT EXISTS item_embeddings USING vec0(
item_id TEXT PRIMARY KEY,
embedding float[{dimensions}]
);
-- Mapping table to join vectors with scope for filtering
CREATE TABLE IF NOT EXISTS embedding_meta (
item_id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_emeta_scope ON embedding_meta(scope);
Separate database file (_vec.db) to avoid extension compatibility issues with
the existing sync SQLite adapter used by ItemsStore.
Query pattern:
SELECT em.item_id, v.distance
FROM item_embeddings v
JOIN embedding_meta em ON em.item_id = v.item_id
WHERE em.scope = ?
AND v.embedding MATCH ?
ORDER BY v.distance
LIMIT ?
New test file: tests/providers/memory/memoryfs/embedding-store.test.ts
- Test CRUD operations (upsert, find, delete)
- Test similarity search returns correct ordering
- Test scope filtering
- Test
listUnembedded()for backfill - Uses temp file for database
Step 4: Extend MemoryQuery with optional embedding field
Modified file: src/providers/memory/types.ts
export interface MemoryQuery {
scope: string;
query?: string;
limit?: number;
tags?: string[];
agentId?: string;
/** Pre-computed embedding vector for semantic search (optional). */
embedding?: Float32Array;
}
Backward-compatible — existing providers ignore the field.
Step 5: Integrate embeddings into MemoryFS provider
Modified file: src/providers/memory/memoryfs/provider.ts
Changes to create(config):
- Initialize
EmbeddingClientfrom config/env vars - Initialize
EmbeddingStorealongsideItemsStore - On
write(): generate embedding →embeddingStore.upsert() - On
memorize(): batch-generate embeddings for all new items - On
query():- If
q.embeddingprovided →embeddingStore.findSimilar()→ join with salience - Else if
q.queryprovided → existing LIKE search (fallback) - Rank: combine
similarity = 1 / (1 + distance)with existingsalienceScore()
- If
- On
delete(): also delete from embedding store
The salience formula in salience.ts already accepts a similarity parameter —
pass computed similarity instead of the hardcoded 1.0 currently used.
Modified file: src/providers/memory/memoryfs/items-store.ts
Add getByIds(ids: string[]): MemoryFSItem[] — batch lookup by ID list
(needed to hydrate vector search results into full items).
Step 6: Update memory recall to use embeddings
Modified file: src/host/memory-recall.ts
export interface MemoryRecallConfig {
enabled: boolean;
limit: number;
scope: string;
/** Embedding client for semantic search (falls back to keywords if absent). */
embeddingClient?: EmbeddingClient;
}
Changes to recallMemoryForMessage():
- If
embeddingClient?.available→ embed user message → pass{ embedding }in query - Else → fall back to existing
extractQueryTerms()+ keyword search - Logging: add
strategy: 'embedding' | 'keyword'to recall log events
Modified file: src/host/server-completions.ts
Create EmbeddingClient instance from config and pass to memory recall:
import { createEmbeddingClient } from '../utils/embedding-client.js';
const embeddingClient = createEmbeddingClient({
model: config.history.embedding_model,
dimensions: config.history.embedding_dimensions,
});
const recallConfig: MemoryRecallConfig = {
enabled: config.history.memory_recall,
limit: config.history.memory_recall_limit,
scope: config.history.memory_recall_scope,
embeddingClient,
};
Step 7: Add config fields for embeddings
Modified file: src/config.ts
Add to the history strict object:
embedding_model: z.string().default('text-embedding-3-small'),
embedding_dimensions: z.number().int().min(64).max(4096).default(1536),
Modified file: src/types.ts
Add to Config.history:
embedding_model: string;
embedding_dimensions: number;
Step 8: Background backfill for existing memories
Modified file: src/providers/memory/memoryfs/provider.ts
After initialization, kick off a fire-and-forget backfill:
async function backfillEmbeddings(
store: ItemsStore,
embeddingStore: EmbeddingStore,
client: EmbeddingClient,
batchSize = 50,
) {
if (!client.available) return;
const unembedded = embeddingStore.listUnembedded(batchSize);
if (unembedded.length === 0) return;
const items = store.getByIds(unembedded);
const vectors = await client.embed(items.map(i => i.content));
for (let i = 0; i < items.length; i++) {
embeddingStore.upsert(items[i].id, vectors[i]);
}
}
- Runs once on provider creation (non-blocking)
- Processes in batches of 50
- Logs progress and errors
- Subsequent queries will have embeddings available
Step 9: Tests
New test files:
tests/utils/embedding-client.test.ts— Client unit teststests/providers/memory/memoryfs/embedding-store.test.ts— Vector store tests
Modified test files:
tests/host/memory-recall.test.ts— Add embedding-based recall tests:- Embedding available → uses semantic search
- Embedding unavailable → falls back to keywords
- Mock embedding client
tests/config-history.test.ts— Update default config assertion with new fieldstests/providers/memory/memoryfs/— Update provider tests:- Test write generates embedding when client available
- Test query with embedding field uses vector search
- Test graceful fallback when no embedding client
Step 10: Journal and lessons
Update .claude/journal/ and .claude/lessons/ per protocol before committing.
File Change Summary
| File | Action | Description |
|---|---|---|
package.json | modify | Add @dao-xyz/sqlite3-vec |
src/utils/embedding-client.ts | new | Standalone OpenAI embedding client |
src/providers/memory/memoryfs/embedding-store.ts | new | sqlite-vec vector store |
src/providers/memory/types.ts | modify | Add optional embedding to MemoryQuery |
src/providers/memory/memoryfs/provider.ts | modify | Integrate embedding on write/query |
src/providers/memory/memoryfs/items-store.ts | modify | Add getByIds() batch method |
src/host/memory-recall.ts | modify | Embed user message, pass to query, fallback |
src/host/server-completions.ts | modify | Create embedding client, pass to recall |
src/config.ts | modify | Add embedding_model, embedding_dimensions |
src/types.ts | modify | Add embedding config to Config.history |
tests/utils/embedding-client.test.ts | new | Embedding client unit tests |
tests/providers/memory/memoryfs/embedding-store.test.ts | new | Vector store tests |
tests/host/memory-recall.test.ts | modify | Add embedding recall tests |
tests/config-history.test.ts | modify | Update default config assertion |
Key Design Decisions
-
Separate
EmbeddingClientutility (not extending LLMProvider) — embeddings are request/response, not streaming chat. Different API shape entirely. -
Separate
_vec.dbfile — avoids compatibility issues between the vec extension and the existing sync SQLite adapter. The vec extension needs to be loaded via@dao-xyz/sqlite3-vec, not the genericopenDatabase(). -
MemoryQuery.embeddingoptional field (not new interface method) — backward compatible, all existing providers and callers continue to work unchanged. -
FTS5/LIKE fallback — when
OPENAI_API_KEYis absent, the system degrades to keyword search silently. No errors, no broken functionality. -
Configurable model + dimensions — defaulting to
text-embedding-3-small(1536 dims, $0.02/1M tokens). Can set to any OpenAI-compatible embedding endpoint via config. -
Background backfill — existing memories get embeddings generated lazily after startup. Non-blocking, non-critical.
-
Similarity × salience scoring — vector distance maps to
similarity = 1 / (1 + distance), then feeds into the existingsalienceScore()formula. Combines semantic relevance with reinforcement frequency and recency. -
EmbeddingClient created in two places — MemoryFS provider (for write-time embedding) and server-completions (for query-time embedding). Both are stateless OpenAI HTTP clients reading from the same env vars. This avoids threading the embedding client through the provider registry.
What's inside
10 implementation steps, 1 architecture diagram, 3 code interfaces, 8 key design decisions, 1 file change table
Change this for your project
- Replace
project-ax/axwith your own repository name - Replace
src/providers/memory/memoryfs/with your own memory provider path - Replace
src/host/memory-recall.tswith your own recall module path - Replace
text-embedding-3-smallwith your preferred embedding model
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Separate vector store into its own database file to avoid extension compatibility issues
- Graceful degradation to keyword search when API key is absent, no errors thrown
- Background backfill for existing records runs non-blocking after startup
Related Documents
Building SupportX AI Assist: A Multi-Agent IT Support System
Describes building a multi-agent IT support system with AutoGen, Azure AI Search, and Gemini embeddings for instant issue resolution and automatic escalation.
Intelligent Document Query Platform — GitHub-ready Low-Level Design (LLD)
Provides a copy-ready low-level design for a serverless document query platform with vector search and LLM integration.
Graph Matching with Topological Features
Teaches enhanced graph matching by combining spatial distances with node2vec and commute times embeddings, then applying the Hungarian algorithm.
Pulse — Life Cofounder | Build Log
Documents a full-stack monorepo that ingests LinkedIn and GitHub data, generates embeddings in-browser, and provides a RAG chat with an AI cofounder.