codex-memory 3.0.15

A simple memory storage service with MCP interface for Claude Desktop
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;

// Import memory tiering types
// Note: Memory tier system has been removed - this is now a simple storage system

/// Enhanced memory structure for text storage with context and summary
/// Implements encoding specificity principle (Tulving & Thomson, 1973)
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Memory {
    pub id: Uuid,
    pub content: String,
    pub content_hash: String, // Simple content hash for basic deduplication
    // Removed context_fingerprint - simplified to use only content_hash
    pub tags: Vec<String>,
    pub context: String,           // Required: What is being stored and why
    pub summary: String,           // Required: Brief summary (120 words or less)
    pub chunk_index: Option<i32>,  // Index of this chunk (1-based, null for non-chunked content)
    pub total_chunks: Option<i32>, // Total number of chunks in the set
    pub parent_id: Option<Uuid>,   // ID of the parent document (for linking chunks together)
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl Memory {
    /// Create a new memory with the given content
    /// Implements encoding specificity principle - context matters for retrieval
    pub fn new(
        content: String,
        context: String,
        summary: String,
        tags: Option<Vec<String>>,
    ) -> Self {
        use sha2::{Digest, Sha256};

        // Simple content hash for basic deduplication
        let mut content_hasher = Sha256::new();
        content_hasher.update(content.as_bytes());
        let content_hash = hex::encode(content_hasher.finalize());

        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            content,
            content_hash,
            tags: tags.unwrap_or_default(),
            context,
            summary,
            chunk_index: None,
            total_chunks: None,
            parent_id: None,
            created_at: now,
            updated_at: now,
        }
    }

    /// Create a new chunked memory with parent reference
    /// Preserves context specificity for chunked content
    pub fn new_chunk(
        content: String,
        context: String,
        summary: String,
        tags: Option<Vec<String>>,
        chunk_index: i32,
        total_chunks: i32,
        parent_id: Uuid,
    ) -> Self {
        use sha2::{Digest, Sha256};

        // Simple content hash for basic deduplication
        let mut content_hasher = Sha256::new();
        content_hasher.update(content.as_bytes());
        let content_hash = hex::encode(content_hasher.finalize());

        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            content,
            content_hash,
            tags: tags.unwrap_or_default(),
            context,
            summary,
            chunk_index: Some(chunk_index),
            total_chunks: Some(total_chunks),
            parent_id: Some(parent_id),
            created_at: now,
            updated_at: now,
        }
    }

    // Removed complex context fingerprint - simplified system uses only content_hash

    // Removed chunk context fingerprint - simplified system

    /// Check semantic similarity between two memories
    /// Implements transfer appropriate processing - matching encoding and retrieval contexts
    pub fn is_semantically_similar(&self, other: &Memory, similarity_threshold: f64) -> bool {
        // For now, implement simple context similarity
        // In a full implementation, this would use embeddings
        let content_similarity = self.simple_text_similarity(&self.content, &other.content);
        let context_similarity = self.simple_text_similarity(&self.context, &other.context);

        // Combined similarity with context weighting (encoding specificity)
        let combined_similarity = (content_similarity * 0.6) + (context_similarity * 0.4);
        combined_similarity >= similarity_threshold
    }

    /// Simple text similarity for semantic comparison
    /// Production version would use proper embeddings
    fn simple_text_similarity(&self, text1: &str, text2: &str) -> f64 {
        use std::collections::HashSet;

        let words1: HashSet<&str> = text1.split_whitespace().collect();
        let words2: HashSet<&str> = text2.split_whitespace().collect();

        let intersection = words1.intersection(&words2).count();
        let union = words1.union(&words2).count();

        if union == 0 {
            0.0
        } else {
            intersection as f64 / union as f64
        }
    }
}

/// Simple storage statistics
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageStats {
    pub total_memories: i64,
    pub table_size: String,
    pub last_memory_created: Option<DateTime<Utc>>,
}

/// Search metadata indicating which search stage produced results
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchMetadata {
    pub stage_used: u8,
    pub stage_description: String,
    pub threshold_used: f64,
    pub total_results: usize,
}

/// Combined search results with metadata
#[derive(Debug, Clone)]
pub struct SearchResultWithMetadata {
    pub results: Vec<SearchResult>,
    pub metadata: SearchMetadata,
}

/// Search parameters for semantic similarity search
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchParams {
    pub query: String,
    pub tag_filter: Option<Vec<String>>,
    pub use_tag_embedding: bool,
    pub use_content_embedding: bool,
    pub similarity_threshold: f64,
    pub max_results: usize,
    pub search_strategy: SearchStrategy,
    pub boost_recent: bool,
    pub tag_weight: f64,
    pub content_weight: f64,
}

impl Default for SearchParams {
    fn default() -> Self {
        Self {
            query: String::new(),
            tag_filter: None,
            use_tag_embedding: true,
            use_content_embedding: true,
            similarity_threshold: 0.7,
            max_results: 10,
            search_strategy: SearchStrategy::Hybrid,
            boost_recent: false,
            tag_weight: 0.4,
            content_weight: 0.6,
        }
    }
}

/// Search strategy options
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SearchStrategy {
    TagsFirst,
    ContentFirst,
    Hybrid,
}

/// Search result with similarity scores
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
    pub memory: Memory,
    pub tag_similarity: Option<f64>,
    pub content_similarity: Option<f64>,
    pub combined_score: f64,
    pub semantic_cluster: Option<i32>,
}

impl SearchResult {
    pub fn new(
        memory: Memory,
        tag_similarity: Option<f64>,
        content_similarity: Option<f64>,
        semantic_cluster: Option<i32>,
        tag_weight: f64,
        content_weight: f64,
    ) -> Self {
        let combined_score = Self::calculate_combined_score(
            tag_similarity,
            content_similarity,
            tag_weight,
            content_weight,
        );

        Self {
            memory,
            tag_similarity,
            content_similarity,
            combined_score,
            semantic_cluster,
        }
    }

    fn calculate_combined_score(
        tag_similarity: Option<f64>,
        content_similarity: Option<f64>,
        tag_weight: f64,
        content_weight: f64,
    ) -> f64 {
        let tag_score = tag_similarity.unwrap_or(0.0) * tag_weight;
        let content_score = content_similarity.unwrap_or(0.0) * content_weight;
        tag_score + content_score
    }
}