Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zeroclaw-labs/zeroclaw/llms.txt

Use this file to discover all available pages before exploring further.

Memory System

The Memory system provides persistent storage for facts, decisions, and conversation context. Multiple backends (Markdown, SQLite, vector databases) can be used interchangeably through a uniform trait interface.

Architecture Overview

Memory Trait

All backends implement the Memory trait from src/memory/traits.rs:
#[async_trait]
pub trait Memory: Send + Sync {
    /// Backend name
    fn name(&self) -> &str;

    /// Store a memory entry, optionally scoped to a session
    async fn store(
        &self,
        key: &str,
        content: &str,
        category: MemoryCategory,
        session_id: Option<&str>,
    ) -> anyhow::Result<()>;

    /// Recall memories matching a query (keyword search)
    async fn recall(
        &self,
        query: &str,
        limit: usize,
        session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>>;

    /// Get a specific memory by key
    async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>>;

    /// List all memory keys, optionally filtered
    async fn list(
        &self,
        category: Option<&MemoryCategory>,
        session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>>;

    /// Remove a memory by key
    async fn forget(&self, key: &str) -> anyhow::Result<bool>;

    /// Count total memories
    async fn count(&self) -> anyhow::Result<usize>;

    /// Health check
    async fn health_check(&self) -> bool;

    /// Rebuild embeddings (vector backends)
    async fn reindex(
        &self,
        progress_callback: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
    ) -> anyhow::Result<usize> {
        anyhow::bail!("Reindex not supported by {} backend", self.name())
    }
}

Memory Categories

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MemoryCategory {
    /// Long-term facts, preferences, decisions
    Core,
    /// Daily session logs
    Daily,
    /// Conversation context
    Conversation,
    /// User-defined custom category
    Custom(String),
}

Memory Entry

#[derive(Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
    pub id: String,
    pub key: String,
    pub content: String,
    pub category: MemoryCategory,
    pub timestamp: String,
    pub session_id: Option<String>,
    pub score: Option<f64>, // For vector similarity search
}

Markdown Memory Implementation

Real implementation from src/memory/markdown.rs:
pub struct MarkdownMemory {
    workspace_dir: PathBuf,
}

impl MarkdownMemory {
    pub fn new(workspace_dir: &Path) -> Self {
        Self {
            workspace_dir: workspace_dir.to_path_buf(),
        }
    }

    fn memory_dir(&self) -> PathBuf {
        self.workspace_dir.join("memory")
    }

    fn core_path(&self) -> PathBuf {
        self.workspace_dir.join("MEMORY.md")
    }

    fn daily_path(&self) -> PathBuf {
        let date = Local::now().format("%Y-%m-%d").to_string();
        self.memory_dir().join(format!("{date}.md"))
    }

    async fn append_to_file(&self, path: &Path, content: &str) -> anyhow::Result<()> {
        self.ensure_dirs().await?;

        let existing = if path.exists() {
            fs::read_to_string(path).await.unwrap_or_default()
        } else {
            String::new()
        };

        let updated = if existing.is_empty() {
            let header = if path == self.core_path() {
                "# Long-Term Memory\n\n"
            } else {
                let date = Local::now().format("%Y-%m-%d").to_string();
                &format!("# Daily Log — {date}\n\n")
            };
            format!("{header}{content}\n")
        } else {
            format!("{existing}\n{content}\n")
        };

        fs::write(path, updated).await?;
        Ok(())
    }
}

#[async_trait]
impl Memory for MarkdownMemory {
    fn name(&self) -> &str {
        "markdown"
    }

    async fn store(
        &self,
        key: &str,
        content: &str,
        category: MemoryCategory,
        _session_id: Option<&str>,
    ) -> anyhow::Result<()> {
        let entry = format!("- **{key}**: {content}");
        
        let path = match category {
            MemoryCategory::Core => self.core_path(),
            MemoryCategory::Daily => self.daily_path(),
            MemoryCategory::Conversation => {
                // Store in daily log with conversation prefix
                let entry = format!("- [Conversation] **{key}**: {content}");
                self.append_to_file(&self.daily_path(), &entry).await?;
                return Ok(());
            }
            MemoryCategory::Custom(ref name) => {
                self.memory_dir().join(format!("{}.md", name))
            }
        };
        
        self.append_to_file(&path, &entry).await
    }

    async fn recall(
        &self,
        query: &str,
        limit: usize,
        _session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        let all = self.read_all_entries().await?;
        let query_lower = query.to_lowercase();
        
        let mut matches: Vec<_> = all.into_iter()
            .filter(|entry| {
                entry.content.to_lowercase().contains(&query_lower)
                    || entry.key.to_lowercase().contains(&query_lower)
            })
            .collect();
            
        matches.truncate(limit);
        Ok(matches)
    }

    async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>> {
        let all = self.read_all_entries().await?;
        Ok(all.into_iter().find(|e| e.key == key))
    }

    async fn list(
        &self,
        category: Option<&MemoryCategory>,
        _session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        let all = self.read_all_entries().await?;
        
        if let Some(cat) = category {
            Ok(all.into_iter().filter(|e| &e.category == cat).collect())
        } else {
            Ok(all)
        }
    }

    async fn forget(&self, key: &str) -> anyhow::Result<bool> {
        // Not implemented for markdown (manual editing)
        Ok(false)
    }

    async fn count(&self) -> anyhow::Result<usize> {
        let all = self.read_all_entries().await?;
        Ok(all.len())
    }

    async fn health_check(&self) -> bool {
        self.workspace_dir.exists()
    }
}

File Layout

workspace/
├── MEMORY.md           # Long-term core memories
└── memory/
    ├── 2026-03-01.md   # Daily log
    ├── 2026-03-02.md
    └── 2026-03-03.md
MEMORY.md:
# Long-Term Memory

- **user_name**: Alice
- **preferred_language**: Rust
- **workspace**: /home/alice/project
memory/2026-03-03.md:
# Daily Log — 2026-03-03

- **bug_fix**: Fixed memory leak in provider connection pool
- **feature**: Added support for Gemini provider
- [Conversation] **context_1**: User asked about trait system

SQLite Memory with Embeddings

The SQLite backend supports vector similarity search:
pub struct SqliteMemory {
    pool: SqlitePool,
    embedding_provider: Option<Arc<dyn EmbeddingProvider>>,
}

impl SqliteMemory {
    pub async fn new(db_path: &Path) -> anyhow::Result<Self> {
        let pool = SqlitePool::connect(db_path.to_str().unwrap()).await?;
        
        // Create schema
        sqlx::query(
            r#"
            CREATE TABLE IF NOT EXISTS memories (
                id TEXT PRIMARY KEY,
                key TEXT NOT NULL,
                content TEXT NOT NULL,
                category TEXT NOT NULL,
                timestamp TEXT NOT NULL,
                session_id TEXT,
                embedding BLOB
            );
            CREATE INDEX IF NOT EXISTS idx_key ON memories(key);
            CREATE INDEX IF NOT EXISTS idx_category ON memories(category);
            CREATE INDEX IF NOT EXISTS idx_session ON memories(session_id);
            "#
        )
        .execute(&pool)
        .await?;
        
        Ok(Self {
            pool,
            embedding_provider: None,
        })
    }
    
    pub fn with_embeddings(mut self, provider: Arc<dyn EmbeddingProvider>) -> Self {
        self.embedding_provider = Some(provider);
        self
    }
}

#[async_trait]
impl Memory for SqliteMemory {
    async fn store(
        &self,
        key: &str,
        content: &str,
        category: MemoryCategory,
        session_id: Option<&str>,
    ) -> anyhow::Result<()> {
        let id = uuid::Uuid::new_v4().to_string();
        let timestamp = chrono::Utc::now().to_rfc3339();
        let category_str = category.to_string();
        
        // Generate embedding if provider available
        let embedding = if let Some(provider) = &self.embedding_provider {
            let vec = provider.embed(content).await?;
            Some(bincode::serialize(&vec)?)
        } else {
            None
        };
        
        sqlx::query(
            r#"
            INSERT INTO memories (id, key, content, category, timestamp, session_id, embedding)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            "#
        )
        .bind(&id)
        .bind(key)
        .bind(content)
        .bind(&category_str)
        .bind(&timestamp)
        .bind(session_id)
        .bind(embedding)
        .execute(&self.pool)
        .await?;
        
        Ok(())
    }
    
    async fn recall(
        &self,
        query: &str,
        limit: usize,
        session_id: Option<&str>,
    ) -> anyhow::Result<Vec<MemoryEntry>> {
        // Use vector similarity if embeddings available
        if let Some(provider) = &self.embedding_provider {
            let query_vec = provider.embed(query).await?;
            
            // Fetch all entries with embeddings
            let rows = if let Some(sid) = session_id {
                sqlx::query_as::<_, (String, String, String, String, String, Option<String>, Option<Vec<u8>>)>(
                    "SELECT id, key, content, category, timestamp, session_id, embedding FROM memories WHERE session_id = ? AND embedding IS NOT NULL"
                )
                .bind(sid)
                .fetch_all(&self.pool)
                .await?
            } else {
                sqlx::query_as(
                    "SELECT id, key, content, category, timestamp, session_id, embedding FROM memories WHERE embedding IS NOT NULL"
                )
                .fetch_all(&self.pool)
                .await?
            };
            
            // Calculate cosine similarity
            let mut scored: Vec<_> = rows.into_iter()
                .filter_map(|(id, key, content, cat, ts, sid, emb)| {
                    let embedding: Vec<f32> = bincode::deserialize(&emb?).ok()?;
                    let score = cosine_similarity(&query_vec, &embedding);
                    
                    Some((score, MemoryEntry {
                        id,
                        key,
                        content,
                        category: parse_category(&cat),
                        timestamp: ts,
                        session_id: sid,
                        score: Some(score),
                    }))
                })
                .collect();
            
            // Sort by similarity descending
            scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
            scored.truncate(limit);
            
            return Ok(scored.into_iter().map(|(_, entry)| entry).collect());
        }
        
        // Fallback to keyword search
        let pattern = format!("%{}%", query);
        let rows = if let Some(sid) = session_id {
            sqlx::query_as(
                "SELECT id, key, content, category, timestamp, session_id FROM memories WHERE session_id = ? AND (content LIKE ? OR key LIKE ?) LIMIT ?"
            )
            .bind(sid)
            .bind(&pattern)
            .bind(&pattern)
            .bind(limit as i64)
            .fetch_all(&self.pool)
            .await?
        } else {
            sqlx::query_as(
                "SELECT id, key, content, category, timestamp, session_id FROM memories WHERE content LIKE ? OR key LIKE ? LIMIT ?"
            )
            .bind(&pattern)
            .bind(&pattern)
            .bind(limit as i64)
            .fetch_all(&self.pool)
            .await?
        };
        
        Ok(rows.into_iter().map(|(id, key, content, cat, ts, sid)| MemoryEntry {
            id,
            key,
            content,
            category: parse_category(&cat),
            timestamp: ts,
            session_id: sid,
            score: None,
        }).collect())
    }
    
    async fn reindex(
        &self,
        progress_callback: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
    ) -> anyhow::Result<usize> {
        let provider = self.embedding_provider.as_ref()
            .ok_or_else(|| anyhow::anyhow!("No embedding provider configured"))?;
        
        // Fetch all entries without embeddings
        let rows: Vec<(String, String)> = sqlx::query_as(
            "SELECT id, content FROM memories WHERE embedding IS NULL"
        )
        .fetch_all(&self.pool)
        .await?;
        
        let total = rows.len();
        
        for (i, (id, content)) in rows.into_iter().enumerate() {
            let vec = provider.embed(&content).await?;
            let embedding = bincode::serialize(&vec)?;
            
            sqlx::query("UPDATE memories SET embedding = ? WHERE id = ?")
                .bind(embedding)
                .bind(&id)
                .execute(&self.pool)
                .await?;
            
            if let Some(ref cb) = progress_callback {
                cb(i + 1, total);
            }
        }
        
        Ok(total)
    }
}

fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
    (dot / (mag_a * mag_b)) as f64
}

Session Management

Memories can be scoped to conversation sessions:
pub struct Session {
    pub id: String,
    pub user_id: String,
    pub channel: String,
    pub started_at: DateTime<Utc>,
    pub last_activity: DateTime<Utc>,
}

pub struct SessionManager {
    sessions: Arc<RwLock<HashMap<String, Session>>>,
}

impl SessionManager {
    pub fn get_or_create(&self, user_id: &str, channel: &str) -> String {
        let key = format!("{}:{}", channel, user_id);
        let mut sessions = self.sessions.write().unwrap();
        
        if let Some(session) = sessions.get_mut(&key) {
            session.last_activity = Utc::now();
            return session.id.clone();
        }
        
        let session = Session {
            id: uuid::Uuid::new_v4().to_string(),
            user_id: user_id.to_string(),
            channel: channel.to_string(),
            started_at: Utc::now(),
            last_activity: Utc::now(),
        };
        
        let id = session.id.clone();
        sessions.insert(key, session);
        id
    }
}

Memory Configuration

[memory]
backend = "sqlite"
path = "~/.local/share/zeroclaw/memory.db"

# Optional embedding provider for semantic search
[memory.embeddings]
provider = "openai"
model = "text-embedding-3-small"

Memory Lifecycle

Best Practices

Storage Strategy

  • Core: Long-term facts, user preferences, important decisions
  • Daily: Session logs, temporary context, work-in-progress
  • Conversation: Short-term context, current thread state

Recall Optimization

  • Use embeddings for semantic search
  • Limit results to relevant subset (5-10 entries)
  • Scope to session when appropriate
  • Prefer specific queries over broad searches

Maintenance

  • Periodically review and prune daily logs
  • Migrate important daily entries to core
  • Reindex embeddings after model changes
  • Back up memory database regularly

Next Steps

  • Security - Security architecture and policy enforcement