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 threads
  • role"user" or "assistant"
  • content – The message text
  • createdAt / updatedAt – Timestamps for TTL and ordering
  • embeddingId – Optional foreign key linking to Qdrant vector
  • metadata – 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:

  1. Primary memory table – Stores the full message record
  2. 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")
  • NEAR operators 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.


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 embeddingId as the point ID
  • Executes similarity searches with client.search(collection, ...)
  • Handles batch operations for conversation-wide deletion

The embedding flow works as follows:

  1. User requests vector search (or sets embed: true on insert)
  2. Text is sent to the configured embedding provider (OpenAI, Ollama, etc.)
  3. Resulting vector is normalized and stored in Qdrant
  4. embeddingId in 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 Qdrant
  • searchMemoryByText({ conversationId, query, limit }) – Queries FTS5, returns ranked results
  • searchMemoryByVector({ conversationId, query, topK }) – Generates embedding, searches Qdrant, merges with SQLite metadata
  • deleteConversationMemory(conversationId) – Cascading delete across both stores

The service layer also enforces PII handling via feature flags:

  • PII_REDACTION_ENABLED – Masks sensitive data before storage
  • PII_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
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 embeddingId enables 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.ts validates all entries at the API boundary
  • src/lib/db/memoryVec.ts manages both SQLite operations and Qdrant client interactions
  • src/app/api/v1/memory/route.ts exposes REST endpoints memory CRUD and search
  • src/lib/memory/index.ts provides 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →