OmniRoute's Persistent Conversational Memory System Architecture Explained
OmniRoute implements a dual-store memory architecture combining SQLite for canonical persistence and Qdrant for vector search, with FTS5 enabling fast full-text retrieval.
The persistent conversational memory system in OmniRoute (diegosouzapw/OmniRoute) gives AI assistants long-term recall across sessions. Every user message and assistant response is durably stored, searchable by keyword, and retrievable by semantic similarity. This article breaks down the five-layer architecture that makes this possible.
Schema and Validation Layer
OmniRoute enforces data integrity through a Zod-based schema defined in src/shared/schemas/memory.ts. The MemoryEntry type validates every memory record before it reaches the database.
A memory entry includes:
id– Unique identifier (UUID)conversationId– Groups messages into conversation threadsrole–"user"or"assistant"content– The message textcreatedAt/updatedAt– Timestamps for TTL and orderingembeddingId– Optional foreign key linking to Qdrant vectormetadata– Extensible JSON field for feature flags
This schema acts as the contract between the API layer and storage backends. Invalid payloads are rejected at the boundary before any database write occurs.
SQLite: The Source of Truth
The canonical store lives in a local SQLite database (~/.omniroute/memory.db). In src/lib/db/memoryVec.ts, the getDbInstance() helper manages connection pooling and migrations.
Two storage mechanisms coexist in SQLite:
- Primary
memorytable – Stores the full message record - FTS5 virtual table (
memory_fts) – Enables full-text search via SQLite's built-in extension
When a row inserts into memory, a database trigger automatically populates memory_fts. This synchronous indexing guarantees keyword search results remain consistent with the primary data.
The FTS5 implementation supports:
- Prefix matching (
weather*) - Phrase queries (
"machine learning") NEARoperators for proximity search
Because SQLite is embedded, the memory survives process restarts, container replacements, and network partitions. A single file encapsulates the entire conversation history.
Qdrant: Vector Embeddings for Semantic Search
For semantic similarity retrieval, OmniRoute delegates to Qdrant, a self-hosted vector database. The same memoryVec.ts module exports a client wrapper that:
- Upserts embeddings with
embeddingIdas the point ID - Executes similarity searches with
client.search(collection, ...) - Handles batch operations for conversation-wide deletion
The embedding flow works as follows:
- User requests vector search (or sets
embed: trueon insert) - Text is sent to the configured embedding provider (OpenAI, Ollama, etc.)
- Resulting vector is normalized and stored in Qdrant
embeddingIdin SQLite references the Qdrant point
Qdrant is not the source of truth. If vectors are lost, a background job re-embeds messages from SQLite. This rebuildability decouples durability from vector operations.
REST API Endpoints
The memory system exposes a unified interface through src/app/api/v1/memory/route.ts. This Next.js API route handles:
| Method | Operation |
|---|---|
POST |
Create a new memory entry |
GET |
Retrieve entries (with optional ?text= or ?vector= query) |
DELETE |
Remove entries by ID or conversation ID |
Endpoints translate HTTP parameters into validated MemoryEntry objects, then delegate to the service layer. Response payloads include both the SQLite record and, for vector queries, the Qdrant similarity score.
Service Layer: Unified Retrieval
The high-level API lives in src/lib/memory/index.ts. These helpers orchestrate cross-store operations so chat handlers need not understand the dual-backend architecture.
Key exports include:
addMemoryEntry(entry)– Persists to SQLite, optionally embeds to QdrantsearchMemoryByText({ conversationId, query, limit })– Queries FTS5, returns ranked resultssearchMemoryByVector({ conversationId, query, topK })– Generates embedding, searches Qdrant, merges with SQLite metadatadeleteConversationMemory(conversationId)– Cascading delete across both stores
The service layer also enforces PII handling via feature flags:
PII_REDACTION_ENABLED– Masks sensitive data before storagePII_RESPONSE_SANITIZATION– Filters retrieved memories in responses
Both default to false, requiring explicit opt-in.
Practical Usage Examples
Store a new message
import { addMemoryEntry } from '@/lib/memory';
await addMemoryEntry({
conversationId: 'conv-123',
role: 'user',
content: 'What is the weather in Paris?',
embed: true // Triggers vector embedding
});
Search by keyword
import { searchMemoryByText } from '@/lib/memory';
const hits = await searchMemoryByText({
conversationId: 'conv-123',
query: 'weather',
limit: 5,
});
// Returns: Array of MemoryEntry with FTS5 rank
Semantic similarity search
import { searchMemoryByVector } from '@/lib/memory';
const similar = await searchMemoryByVector({
conversationId: 'conv-123',
query: 'forecast for tomorrow', // Embedded at query time
topK: 3,
});
// Returns: MemoryEntry objects with similarity scores
Delete conversation history
import { deleteConversationMemory } from '@/lib/memory';
await deleteConversationMemory('conv-123');
// Removes from SQLite, Qdrant, and FTS5 in one atomic operation
Key Design Decisions
- SQLite as source of truth – Guarantees durability regardless of vector store health
- FTS5 for text, Qdrant for vectors – Optimizes each workload for the right engine
- Optional embeddings – Reduces latency and cost for scenarios not needing semantic search
- Foreign key linkage – SQLite
embeddingIdenables cross-store consistency checks - Feature-flag safety – PII controls are explicit, not default-enabled
Summary
- OmniRoute's persistent conversational memory combines SQLite (canonical storage + FTS5 full-text search) with Qdrant (vector embeddings for semantic retrieval)
- The Zod schema in
src/shared/schemas/memory.tsvalidates all entries at the API boundary src/lib/db/memoryVec.tsmanages both SQLite operations and Qdrant client interactionssrc/app/api/v1/memory/route.tsexposes REST endpoints memory CRUD and searchsrc/lib/memory/index.tsprovides unified helpers that orchestrate cross-store operations- Embeddings are optional and rebuildable, with SQLite serving as the durable source of truth
Frequently Asked Questions
How does OmniRoute handle memory during container restarts?
All messages persist to a local SQLite file at ~/.omniroute/memory.db. Since SQLite is embedded and file-based, the database survives process restarts, container recreation, and host migrations without additional configuration. The Qdrant vector store can be entirely rebuilt from SQLite if needed.
What is the difference between searchMemoryByText and searchMemoryByVector?
searchMemoryByText uses SQLite's built-in FTS5 virtual table for keyword-based retrieval—fast, exact, and substring matching. searchMemoryByVector generates an embedding from the query text, searches Qdrant for nearest neighbors, and returns semantically similar messages even without keyword overlap. The service layer can merge both result sets for hybrid retrieval.
Can I disable vector embeddings to reduce latency?
Yes. The embed parameter in addMemoryEntry defaults to false. When omitted, only SQLite storage occurs. This is useful for high-throughput scenarios where semantic search isn't required, cutting embedding API costs and insertion latency.
Where are the PII controls implemented?
PII redaction and sanitization are gated by PII_REDACTION_ENABLED and PII_RESPONSE_SANITIZATION flags in src/lib/memory/index.ts. When enabled, sensitive data is masked before SQLite insertion and/or filtered from retrieval results. Both flags default to false, requiring explicit environment configuration to activate.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →