How Cherry Studio's Memory System Stores and Retrieves Conversation Context

Cherry Studio uses a three-layer architecture that extracts personal facts from conversations using LLM prompts, stores them in a local SQLite database with optional vector embeddings, and retrieves relevant context through hybrid similarity search.

Cherry Studio is an open-source AI desktop application that implements a persistent memory system to maintain conversation context across sessions. The memory system stores and retrieves conversation context by extracting personal facts, deduplicating entries, and enabling vector-based similarity search. This article examines the implementation based on the cherryhq/cherry-studio source code.

Architecture Overview

The memory implementation spans three distinct layers across the renderer and main processes:

Layer Responsibility Core Implementation
1. Fact extraction The LLM is prompted to pull personal facts from the chat. The prompt is defined in memory-prompts.ts and the output is validated with Zod schemas (FactRetrievalSchema). src/renderer/src/utils/memory-prompts.ts
2. Storage & indexing Extracted facts are turned into memory items and stored in a local SQLite database (libsql). Each item is hashed, optionally embedded with a vector model, and indexed for fast similarity search. src/main/services/memory/MemoryService.ts (database schema, embedding generation, deduplication, vector index)
3. Retrieval When a new user query arrives, the renderer asks the main process for a similarity search. The search can be a plain keyword match or a hybrid vector‑plus‑filter query, returning the most relevant memories. src/renderer/src/services/MemoryService.ts (search API) and src/main/services/memory/MemoryService.ts (hybridSearch implementation)

Fact Extraction Layer

LLM Prompting and Schema Validation

The system extracts personal facts by sending the conversation transcript to an LLM with a specialized prompt. In src/renderer/src/utils/memory-prompts.ts, the factExtractionPrompt instructs the model to identify user-specific information such as preferences, personal details, or prior context.

The LLM response must conform to FactRetrievalSchema, a Zod schema that validates the JSON structure. This ensures the output contains a list of fact strings before they proceed to storage.

import { FactRetrievalSchema } from '@renderer/utils/memory-prompts'

async function extractFacts(transcript: string) {
  const llmResponse = await llm.invoke({
    prompt: factExtractionPrompt,
    variables: { transcript }
  })
  const parsed = FactRetrievalSchema.safeParse(JSON.parse(llmResponse))
  if (!parsed.success) throw new Error('Invalid fact format')
  return parsed.data.facts               // => string[]
}

Storage and Indexing Layer

Database Schema and Initialization

The main process handles persistence through src/main/services/memory/MemoryService.ts. During initialization, the service creates SQLite tables for memories and memory_history, along with vector indexes for similarity search. The database uses libsql (Turso's SQLite fork) to support vector extensions.

Deduplication and Hashing

Before inserting a new fact, the system computes a SHA-256 hash of the trimmed fact string. If an existing non-deleted record shares the same hash, the insertion is skipped. This prevents duplicate entries from cluttering the memory store.

// Simplified deduplication logic from MemoryService.ts
const hash = crypto.createHash('sha256').update(fact.trim()).digest('hex')
const existing = await db.query('SELECT id FROM memories WHERE hash = ? AND deleted_at IS NULL', [hash])
if (existing.length > 0) return // Skip duplicate

Vector Embeddings and Similarity Guard

If an embedding model is configured, generateEmbedding() creates a dense vector (default dimension 1536) for each fact. Before final insertion, a similarity guard runs a hybrid vector search against existing memories. If any existing memory scores ≥ 0.85 similarity, the new fact is rejected as redundant.

// From src/main/services/memory/MemoryService.ts
async function add(fact: string, options: MemoryOptions) {
  const embedding = await generateEmbedding(fact)
  const similar = await hybridSearch(fact, embedding, { threshold: 0.85 })
  if (similar.length > 0) return // Too similar to existing memory
  
  const id = crypto.randomUUID()
  await db.insert('memories', { id, content: fact, embedding, hash, ... })
  await db.insert('memory_history', { memory_id: id, action: 'ADD' })
}

Retrieval Layer

Hybrid Search Implementation

When the UI needs context for a new user message, the renderer calls MemoryService.search(query, options). This forwards the request via IPC to the main process's hybridSearch implementation.

The search combines:

  • Vector similarity: Cosine similarity against stored embeddings (if available)
  • Metadata filtering: By userId, agentId, or other tags
  • Keyword matching: Full-text search as a fallback or boost

Results are ranked by composite score and returned to the renderer, where they are injected into the next LLM prompt.

// Renderer-side search usage
async function getRelevantMemories(message: string, userId: string) {
  const memSvc = MemoryService.getInstance()
  memSvc.setCurrentUser(userId)

  const result = await memSvc.search(message, {
    userId,
    limit: 5,
    threshold: 0.5
  })

  return result.results.map(m => m.memory)   // array of matching memory strings
}

Code Examples

Extracting Facts from a Chat Transcript

import { FactRetrievalSchema } from '@renderer/utils/memory-prompts'

async function extractFacts(transcript: string) {
  const llmResponse = await llm.invoke({
    prompt: factExtractionPrompt,
    variables: { transcript }
  })
  const parsed = FactRetrievalSchema.safeParse(JSON.parse(llmResponse))
  if (!parsed.success) throw new Error('Invalid fact format')
  return parsed.data.facts               // => string[]
}

Uses the prompt defined in memory-prompts.ts (lines 92-104).

Adding Extracted Facts to Memory

import MemoryService from '@renderer/services/MemoryService'

async function storeFacts(facts: string[], userId: string) {
  const memSvc = MemoryService.getInstance()
  memSvc.setCurrentUser(userId)

  await memSvc.add(facts, {
    userId,
    agentId: 'assistant-1',
    runId: crypto.randomUUID()
  })
}

Calls the renderer service (src/renderer/src/services/MemoryService.ts lines 31-38).

Searching for Memories When Handling a New User Message

async function getRelevantMemories(message: string, userId: string) {
  const memSvc = MemoryService.getInstance()
  memSvc.setCurrentUser(userId)

  const result = await memSvc.search(message, {
    userId,
    limit: 5,
    threshold: 0.5
  })

  return result.results.map(m => m.memory)   // array of matching memory strings
}

Wraps the search method (renderer) which forwards to the main-process vector search.

Updating Memory Configuration

import store from '@renderer/store'
import { setGlobalMemoryEnabled, updateMemoryConfig } from '@renderer/store/memory'

function changeEmbeddingModel(newModel) {
  store.dispatch(updateMemoryConfig({
    ...store.getState().memory.memoryConfig,
    embeddingModel: newModel
  }))
  // The MemoryService automatically pushes the new config to the main process
}

Triggers updateConfig() in the renderer service (lines 98-110).

Key Implementation Files

File Role
src/renderer/src/utils/memory-prompts.ts Defines LLM prompts and Zod schemas for fact extraction and memory-update instructions.
src/renderer/src/services/MemoryService.ts Front-end façade; handles IPC calls for list/add/search/delete and keeps the current user context.
src/main/services/memory/MemoryService.ts Core backend; creates the SQLite DB, stores memories, generates embeddings, performs deduplication and hybrid vector search.
src/main/services/memory/queries.ts (referenced) SQL statements for table creation, indexes, and CRUD operations.
src/renderer/src/store/memory.ts Redux slice that holds the global memory configuration and enables/disables the feature.
src/renderer/src/hooks/useAppInit.ts Initializes the memory service on app startup and reacts to config changes.

These files together implement a persistent, vector-enabled memory store that extracts personal facts, de-duplicates them, and makes them searchable for context-aware conversations.

Summary

  • Three-layer architecture: Fact extraction via LLM prompts, SQLite storage with vector indexing, and hybrid similarity retrieval.
  • Deduplication: SHA-256 hashing prevents identical fact storage, while vector similarity (≥0.85 threshold) blocks near-duplicate entries.
  • Vector support: Optional embedding generation (default 1536 dimensions) enables semantic similarity search alongside metadata filtering.
  • IPC communication: The renderer process (MemoryService.ts) delegates database operations to the main process via IPC, maintaining separation between UI and data layers.
  • Audit trail: Every memory operation creates a history entry (memory_history table) tracking additions, updates, and deletions.

Frequently Asked Questions

How does Cherry Studio prevent duplicate memories from being stored?

Cherry Studio implements a two-stage deduplication process in src/main/services/memory/MemoryService.ts. First, it computes a SHA-256 hash of the trimmed fact string and checks for existing non-deleted records with the same hash. Second, before insertion, it runs a hybrid vector search with a 0.85 similarity threshold; if any existing memory is semantically similar enough, the new fact is rejected as redundant.

What database does Cherry Studio use for memory storage?

The memory system uses libsql (Turso's SQLite fork) as its storage engine, implemented in src/main/services/memory/MemoryService.ts. This provides native SQLite compatibility while supporting vector extensions for embedding storage and similarity search. The database schema includes memories and memory_history tables with appropriate indexes for fast retrieval.

Can Cherry Studio work without an embedding model?

Yes, the memory system functions in a keyword-only mode when no embedding model is configured. In src/main/services/memory/MemoryService.ts, the hybridSearch method adapts to available resources: if embeddings exist, it performs vector similarity search; otherwise, it falls back to metadata filtering and full-text search. However, semantic similarity detection for deduplication requires embeddings to achieve the 0.85 similarity threshold check.

How is the memory configuration updated at runtime?

Configuration updates flow through the Redux store in src/renderer/src/store/memory.ts and propagate via the renderer's MemoryService. When updateMemoryConfig() is dispatched, the renderer service's updateConfig() method (lines 98-110) pushes the new settings—including embedding model changes—to the main process via IPC. The main process then reinitializes the embedding client and database connection with the updated parameters without requiring an application restart.

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 →