Skip to main content

st/mcp/
wave_memory.rs

1//! Wave-based Memory Manager for Claude Code
2//!
3//! Bridges MCP tools with the MEM8 wave grid for semantic memory storage.
4//! Features: resonance-based retrieval, emotional encoding, temporal decay.
5//!
6//! This is THE memory system for the best Claude Code experience.
7
8use crate::mem8::{FrequencyBand, MemoryWave, WaveGrid};
9use anyhow::{Context, Result};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, RwLock};
16
17/// Memory types mapped to frequency bands
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
19pub enum MemoryType {
20    /// Deep patterns, architecture decisions (Delta: 0-100Hz)
21    Pattern,
22    /// Solutions, breakthroughs (Theta: 100-200Hz)
23    Solution,
24    /// Conversational context, rapport (Alpha: 200-300Hz)
25    Conversation,
26    /// Technical insights, code patterns (Beta: 300-500Hz)
27    Technical,
28    /// Learning moments, aha! insights (Gamma: 500-800Hz)
29    Learning,
30    /// Jokes, shared humor (HyperGamma: 800-1000Hz)
31    Joke,
32}
33
34impl MemoryType {
35    /// Convert to frequency band
36    pub fn to_frequency_band(&self) -> FrequencyBand {
37        match self {
38            Self::Pattern => FrequencyBand::Delta,
39            Self::Solution => FrequencyBand::Theta,
40            Self::Conversation => FrequencyBand::Alpha,
41            Self::Technical => FrequencyBand::Beta,
42            Self::Learning => FrequencyBand::Gamma,
43            Self::Joke => FrequencyBand::HyperGamma,
44        }
45    }
46
47    /// Get center frequency for this memory type
48    pub fn frequency(&self) -> f32 {
49        let (min, max) = self.to_frequency_band().range();
50        (min + max) / 2.0
51    }
52
53    /// Parse from string
54    pub fn parse(s: &str) -> Self {
55        match s.to_lowercase().as_str() {
56            "pattern" | "pattern_insight" => Self::Pattern,
57            "solution" | "breakthrough" => Self::Solution,
58            "conversation" | "rapport" => Self::Conversation,
59            "technical" | "technical_pattern" => Self::Technical,
60            "learning" | "learning_moment" => Self::Learning,
61            "joke" | "shared_joke" => Self::Joke,
62            _ => Self::Technical, // Default
63        }
64    }
65}
66
67/// A memory anchored in the wave grid
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AnchoredMemory {
70    /// Unique identifier
71    pub id: String,
72    /// The actual content
73    pub content: String,
74    /// Keywords for fast lookup
75    pub keywords: Vec<String>,
76    /// Memory type (determines frequency band)
77    pub memory_type: MemoryType,
78    /// Emotional valence (-1.0 to 1.0): negative to positive
79    pub valence: f32,
80    /// Emotional arousal (0.0 to 1.0): calm to excited
81    pub arousal: f32,
82    /// Grid coordinates (semantic position)
83    pub x: u8,
84    pub y: u8,
85    pub z: u16,
86    /// When anchored
87    pub created_at: DateTime<Utc>,
88    /// When last accessed (for reinforcement)
89    pub last_accessed: DateTime<Utc>,
90    /// Access count (reinforcement strength)
91    pub access_count: u32,
92    /// Origin (human, ai:claude, tandem:human:claude)
93    pub origin: String,
94    /// Project path this memory is associated with
95    pub project_path: Option<PathBuf>,
96}
97
98impl AnchoredMemory {
99    /// Calculate semantic coordinates from content + keywords
100    pub fn calculate_coordinates(
101        content: &str,
102        keywords: &[String],
103        memory_type: MemoryType,
104    ) -> (u8, u8, u16) {
105        // X-axis: content hash for semantic distribution
106        let content_hash = Self::hash_string(content);
107        let x = (content_hash % 256) as u8;
108
109        // Y-axis: emotional/type spectrum (different memory types spread across Y)
110        let type_offset = match memory_type {
111            MemoryType::Pattern => 0,
112            MemoryType::Solution => 42,
113            MemoryType::Conversation => 84,
114            MemoryType::Technical => 128,
115            MemoryType::Learning => 170,
116            MemoryType::Joke => 212,
117        };
118        let keyword_hash = keywords.iter().map(|k| Self::hash_string(k)).sum::<u64>();
119        let y = ((type_offset as u64 + keyword_hash % 43) % 256) as u8;
120
121        // Z-axis: temporal depth (newer = higher Z, using timestamp)
122        let now = Utc::now().timestamp() as u64;
123        let z = ((now / 60) % 65536) as u16; // Minutes resolution
124
125        (x, y, z)
126    }
127
128    /// Simple hash function for string
129    fn hash_string(s: &str) -> u64 {
130        s.bytes()
131            .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64))
132    }
133
134    /// Calculate resonance with another memory (0.0 to 1.0)
135    pub fn resonance_with(&self, other: &AnchoredMemory) -> f32 {
136        // Frequency similarity (same type = high resonance)
137        let freq_sim = if self.memory_type == other.memory_type {
138            1.0
139        } else {
140            0.5
141        };
142
143        // Keyword overlap
144        let overlap = self
145            .keywords
146            .iter()
147            .filter(|k| other.keywords.contains(k))
148            .count() as f32;
149        let total = (self.keywords.len() + other.keywords.len()).max(1) as f32;
150        let keyword_sim = 2.0 * overlap / total;
151
152        // Emotional similarity
153        let valence_diff = (self.valence - other.valence).abs();
154        let arousal_diff = (self.arousal - other.arousal).abs();
155        let emotion_sim = 1.0 - (valence_diff + arousal_diff) / 4.0;
156
157        // Spatial proximity in grid
158        let dx = (self.x as i32 - other.x as i32).abs() as f32 / 256.0;
159        let dy = (self.y as i32 - other.y as i32).abs() as f32 / 256.0;
160        let spatial_sim = 1.0 - (dx + dy) / 2.0;
161
162        // Weighted combination
163        0.3 * freq_sim + 0.4 * keyword_sim + 0.2 * emotion_sim + 0.1 * spatial_sim
164    }
165
166    /// Convert to MemoryWave for grid storage
167    pub fn to_wave(&self) -> MemoryWave {
168        let frequency = self.memory_type.frequency();
169        let amplitude = 0.5 + (self.access_count as f32 / 100.0).min(0.5); // 0.5 to 1.0
170
171        let mut wave = MemoryWave::new(frequency, amplitude);
172        wave.valence = self.valence;
173        wave.arousal = self.arousal;
174
175        // Phase encodes temporal relationship (when created)
176        let hours = self.created_at.timestamp() as f32 / 3600.0;
177        wave.phase = (hours % (2.0 * std::f32::consts::PI)).abs();
178
179        wave
180    }
181}
182
183/// Index for fast keyword lookup
184#[derive(Debug, Default, Serialize, Deserialize)]
185struct KeywordIndex {
186    /// Keyword -> list of memory IDs
187    keywords: HashMap<String, Vec<String>>,
188}
189
190impl KeywordIndex {
191    fn add(&mut self, keyword: &str, memory_id: &str) {
192        self.keywords
193            .entry(keyword.to_lowercase())
194            .or_default()
195            .push(memory_id.to_string());
196    }
197
198    fn find(&self, keywords: &[String]) -> Vec<String> {
199        let mut scores: HashMap<String, usize> = HashMap::new();
200
201        for keyword in keywords {
202            if let Some(ids) = self.keywords.get(&keyword.to_lowercase()) {
203                for id in ids {
204                    *scores.entry(id.clone()).or_default() += 1;
205                }
206            }
207        }
208
209        // Sort by score (most matching keywords first)
210        let mut results: Vec<_> = scores.into_iter().collect();
211        results.sort_by(|a, b| b.1.cmp(&a.1));
212        results.into_iter().map(|(id, _)| id).collect()
213    }
214}
215
216/// The unified Wave Memory Manager
217///
218/// This is THE memory system for Claude Code - combining:
219/// - Wave-based semantic storage (fast, meaningful)
220/// - Keyword indexing (instant lookup)
221/// - Resonance search (find similar memories)
222/// - Emotional context (valence + arousal)
223/// - Temporal decay (old memories fade)
224pub struct WaveMemoryManager {
225    /// The wave grid for semantic storage
226    wave_grid: Arc<RwLock<WaveGrid>>,
227    /// All anchored memories
228    memories: HashMap<String, AnchoredMemory>,
229    /// Keyword index for fast lookup
230    keyword_index: KeywordIndex,
231    /// Path to persistence file
232    storage_path: PathBuf,
233    /// Whether changes need saving
234    dirty: bool,
235}
236
237impl WaveMemoryManager {
238    /// Create or load memory manager
239    /// WARNING: This allocates a 4.29 billion voxel grid - use new_test() for tests!
240    pub fn new(storage_dir: Option<&Path>) -> Self {
241        let storage_path = storage_dir
242            .map(|p| p.join(".st").join("mem8").join("wave_memory.m8"))
243            .unwrap_or_else(|| {
244                std::env::current_dir()
245                    .unwrap_or_else(|_| PathBuf::from("."))
246                    .join(".st")
247                    .join("mem8")
248                    .join("wave_memory.m8")
249            });
250
251        let mut manager = Self {
252            wave_grid: Arc::new(RwLock::new(WaveGrid::new())),
253            memories: HashMap::new(),
254            keyword_index: KeywordIndex::default(),
255            storage_path,
256            dirty: false,
257        };
258
259        // Try to load existing memories
260        if let Err(e) = manager.load() {
261            eprintln!("Note: Starting fresh wave memory ({})", e);
262        }
263
264        manager
265    }
266
267    /// Create memory manager with compact grid (256×256×256)
268    /// Use this for daemons and memory-constrained environments
269    pub fn new_compact(storage_dir: Option<&Path>) -> Self {
270        let storage_path = storage_dir
271            .map(|p| p.join(".st").join("mem8").join("wave_memory.m8"))
272            .unwrap_or_else(|| {
273                dirs::home_dir()
274                    .unwrap_or_else(|| PathBuf::from("."))
275                    .join(".mem8")
276                    .join("wave_memory.m8")
277            });
278
279        let mut manager = Self {
280            wave_grid: Arc::new(RwLock::new(WaveGrid::new_compact())),
281            memories: HashMap::new(),
282            keyword_index: KeywordIndex::default(),
283            storage_path,
284            dirty: false,
285        };
286
287        // Try to load existing memories
288        if let Err(e) = manager.load() {
289            eprintln!("Note: Starting fresh wave memory ({})", e);
290        }
291
292        manager
293    }
294
295    /// Create memory manager with smaller grid for testing
296    /// Uses 256×256×256 grid instead of 256×256×65536 (256x smaller)
297    #[cfg(test)]
298    pub fn new_test(storage_dir: Option<&Path>) -> Self {
299        let storage_path = storage_dir
300            .map(|p| p.join(".st").join("mem8").join("wave_memory_test.m8"))
301            .unwrap_or_else(|| {
302                std::env::current_dir()
303                    .unwrap_or_else(|_| PathBuf::from("."))
304                    .join(".st")
305                    .join("mem8")
306                    .join("wave_memory_test.m8")
307            });
308
309        let mut manager = Self {
310            wave_grid: Arc::new(RwLock::new(WaveGrid::new_test())),
311            memories: HashMap::new(),
312            keyword_index: KeywordIndex::default(),
313            storage_path,
314            dirty: false,
315        };
316
317        // Try to load existing memories (same as regular new())
318        let _ = manager.load();
319
320        manager
321    }
322
323    /// Anchor a new memory
324    #[allow(clippy::too_many_arguments)] // Builder pattern alternative considered but this is clearer
325    pub fn anchor(
326        &mut self,
327        content: String,
328        keywords: Vec<String>,
329        memory_type: MemoryType,
330        valence: f32,
331        arousal: f32,
332        origin: String,
333        project_path: Option<PathBuf>,
334    ) -> Result<String> {
335        let id = uuid::Uuid::new_v4().to_string();
336        let (x, y, z) = AnchoredMemory::calculate_coordinates(&content, &keywords, memory_type);
337
338        let memory = AnchoredMemory {
339            id: id.clone(),
340            content,
341            keywords: keywords.clone(),
342            memory_type,
343            valence: valence.clamp(-1.0, 1.0),
344            arousal: arousal.clamp(0.0, 1.0),
345            x,
346            y,
347            z,
348            created_at: Utc::now(),
349            last_accessed: Utc::now(),
350            access_count: 1,
351            origin,
352            project_path,
353        };
354
355        // Store in wave grid
356        let wave = memory.to_wave();
357        if let Ok(mut grid) = self.wave_grid.write() {
358            grid.store(x, y, z, wave);
359        }
360
361        // Index keywords
362        for keyword in &keywords {
363            self.keyword_index.add(keyword, &id);
364        }
365
366        // Store memory
367        self.memories.insert(id.clone(), memory);
368        self.dirty = true;
369
370        Ok(id)
371    }
372
373    /// Find memories by keywords (fast lookup)
374    /// Returns cloned memories to avoid borrow conflicts
375    pub fn find_by_keywords(
376        &mut self,
377        keywords: &[String],
378        max_results: usize,
379    ) -> Vec<AnchoredMemory> {
380        let ids = self.keyword_index.find(keywords);
381        let found_ids: Vec<String> = ids.iter().take(max_results).cloned().collect();
382
383        // Collect results first
384        let results: Vec<AnchoredMemory> = found_ids
385            .iter()
386            .filter_map(|id| self.memories.get(id).cloned())
387            .collect();
388
389        // Update access counts for found memories
390        for id in &found_ids {
391            if let Some(mem) = self.memories.get_mut(id) {
392                mem.access_count += 1;
393                mem.last_accessed = Utc::now();
394                self.dirty = true;
395            }
396        }
397
398        results
399    }
400
401    /// Find memories by resonance (semantic similarity)
402    /// Returns cloned memories with resonance scores
403    pub fn find_by_resonance(
404        &mut self,
405        query_content: &str,
406        query_keywords: &[String],
407        query_type: MemoryType,
408        threshold: f32,
409        max_results: usize,
410    ) -> Vec<(AnchoredMemory, f32)> {
411        // Create a query memory for comparison
412        let (x, y, z) =
413            AnchoredMemory::calculate_coordinates(query_content, query_keywords, query_type);
414        let query = AnchoredMemory {
415            id: String::new(),
416            content: query_content.to_string(),
417            keywords: query_keywords.to_vec(),
418            memory_type: query_type,
419            valence: 0.0,
420            arousal: 0.5,
421            x,
422            y,
423            z,
424            created_at: Utc::now(),
425            last_accessed: Utc::now(),
426            access_count: 0,
427            origin: String::new(),
428            project_path: None,
429        };
430
431        // Calculate resonance with all memories and collect with IDs
432        let mut resonances: Vec<(String, AnchoredMemory, f32)> = self
433            .memories
434            .values()
435            .map(|mem| (mem.id.clone(), mem.clone(), mem.resonance_with(&query)))
436            .filter(|(_, _, r)| *r >= threshold)
437            .collect();
438
439        // Sort by resonance (highest first)
440        resonances.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
441
442        // Get the IDs to update
443        let update_ids: Vec<String> = resonances
444            .iter()
445            .take(max_results)
446            .map(|(id, _, _)| id.clone())
447            .collect();
448
449        // Update access counts
450        for id in &update_ids {
451            if let Some(m) = self.memories.get_mut(id) {
452                m.access_count += 1;
453                m.last_accessed = Utc::now();
454                self.dirty = true;
455            }
456        }
457
458        resonances
459            .into_iter()
460            .take(max_results)
461            .map(|(_, mem, r)| (mem, r))
462            .collect()
463    }
464
465    /// Get wave interference pattern at a location
466    pub fn get_interference(&self, x: u8, y: u8, z: u16, t: f32) -> f32 {
467        if let Ok(grid) = self.wave_grid.read() {
468            grid.calculate_interference(x, y, z, t)
469        } else {
470            0.0
471        }
472    }
473
474    /// Get memory statistics
475    pub fn stats(&self) -> serde_json::Value {
476        let type_counts: HashMap<String, usize> =
477            self.memories.values().fold(HashMap::new(), |mut acc, mem| {
478                *acc.entry(format!("{:?}", mem.memory_type)).or_default() += 1;
479                acc
480            });
481
482        let active_count = if let Ok(grid) = self.wave_grid.read() {
483            grid.active_memory_count()
484        } else {
485            0
486        };
487
488        serde_json::json!({
489            "total_memories": self.memories.len(),
490            "active_waves": active_count,
491            "unique_keywords": self.keyword_index.keywords.len(),
492            "by_type": type_counts,
493            "storage_path": self.storage_path.display().to_string(),
494        })
495    }
496
497    /// Save memories to disk
498    pub fn save(&mut self) -> Result<()> {
499        if !self.dirty {
500            return Ok(());
501        }
502
503        // Ensure directory exists
504        if let Some(parent) = self.storage_path.parent() {
505            fs::create_dir_all(parent).context("Failed to create memory directory")?;
506        }
507
508        // Serialize memories and index
509        let data = serde_json::json!({
510            "version": 1,
511            "memories": self.memories,
512            "keyword_index": self.keyword_index,
513        });
514
515        let json = serde_json::to_string_pretty(&data).context("Failed to serialize memories")?;
516
517        fs::write(&self.storage_path, json).context("Failed to write memory file")?;
518
519        self.dirty = false;
520        eprintln!(
521            "💾 Saved {} memories to {}",
522            self.memories.len(),
523            self.storage_path.display()
524        );
525
526        Ok(())
527    }
528
529    /// Load memories from disk
530    pub fn load(&mut self) -> Result<()> {
531        if !self.storage_path.exists() {
532            return Err(anyhow::anyhow!("No memory file found"));
533        }
534
535        let json = fs::read_to_string(&self.storage_path).context("Failed to read memory file")?;
536
537        let data: serde_json::Value =
538            serde_json::from_str(&json).context("Failed to parse memory file")?;
539
540        // Load memories
541        if let Some(memories) = data.get("memories") {
542            self.memories = serde_json::from_value(memories.clone())
543                .context("Failed to deserialize memories")?;
544        }
545
546        // Load keyword index
547        if let Some(index) = data.get("keyword_index") {
548            self.keyword_index = serde_json::from_value(index.clone())
549                .context("Failed to deserialize keyword index")?;
550        }
551
552        // Rebuild wave grid from memories
553        if let Ok(mut grid) = self.wave_grid.write() {
554            for memory in self.memories.values() {
555                let wave = memory.to_wave();
556                grid.store(memory.x, memory.y, memory.z, wave);
557            }
558        }
559
560        eprintln!(
561            "🧠 Loaded {} memories from {}",
562            self.memories.len(),
563            self.storage_path.display()
564        );
565
566        Ok(())
567    }
568
569    /// Get a memory by ID
570    pub fn get(&self, id: &str) -> Option<&AnchoredMemory> {
571        self.memories.get(id)
572    }
573
574    /// Delete a memory
575    pub fn delete(&mut self, id: &str) -> bool {
576        if let Some(memory) = self.memories.remove(id) {
577            // Note: We don't remove from wave grid (it will decay naturally)
578            // But we do remove from keyword index
579            for keyword in &memory.keywords {
580                if let Some(ids) = self.keyword_index.keywords.get_mut(&keyword.to_lowercase()) {
581                    ids.retain(|i| i != id);
582                }
583            }
584            self.dirty = true;
585            true
586        } else {
587            false
588        }
589    }
590}
591
592impl Drop for WaveMemoryManager {
593    fn drop(&mut self) {
594        // Best effort save on drop
595        let _ = self.save();
596    }
597}
598
599/// Global instance for MCP tool access
600static WAVE_MEMORY: std::sync::OnceLock<std::sync::Mutex<WaveMemoryManager>> =
601    std::sync::OnceLock::new();
602
603/// Get the global wave memory manager
604pub fn get_wave_memory() -> &'static std::sync::Mutex<WaveMemoryManager> {
605    WAVE_MEMORY.get_or_init(|| std::sync::Mutex::new(WaveMemoryManager::new(None)))
606}
607
608/// Initialize wave memory with a specific storage directory
609pub fn init_wave_memory(storage_dir: &Path) {
610    let _ = WAVE_MEMORY.set(std::sync::Mutex::new(WaveMemoryManager::new(Some(
611        storage_dir,
612    ))));
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use tempfile::tempdir;
619
620    #[test]
621    fn test_anchor_and_find() {
622        let dir = tempdir().unwrap();
623        let mut manager = WaveMemoryManager::new_test(Some(dir.path()));
624
625        // Anchor a memory
626        let id = manager
627            .anchor(
628                "The solution to the authentication bug was using JWT refresh tokens".to_string(),
629                vec!["auth".to_string(), "jwt".to_string(), "bug".to_string()],
630                MemoryType::Solution,
631                0.8, // Positive valence
632                0.7, // High arousal (exciting!)
633                "tandem:hue:claude".to_string(),
634                None,
635            )
636            .unwrap();
637
638        assert!(!id.is_empty());
639
640        // Find by keywords
641        let results = manager.find_by_keywords(&["auth".to_string(), "jwt".to_string()], 10);
642        assert_eq!(results.len(), 1);
643        assert!(results[0].content.contains("JWT refresh tokens"));
644    }
645
646    #[test]
647    fn test_resonance_search() {
648        let dir = tempdir().unwrap();
649        let mut manager = WaveMemoryManager::new_test(Some(dir.path()));
650
651        // Anchor several memories
652        manager
653            .anchor(
654                "Rust async/await pattern for error handling".to_string(),
655                vec!["rust".to_string(), "async".to_string(), "error".to_string()],
656                MemoryType::Technical,
657                0.3,
658                0.5,
659                "tandem:hue:claude".to_string(),
660                None,
661            )
662            .unwrap();
663
664        manager
665            .anchor(
666                "Go channels for concurrent error propagation".to_string(),
667                vec![
668                    "go".to_string(),
669                    "channels".to_string(),
670                    "error".to_string(),
671                ],
672                MemoryType::Technical,
673                0.2,
674                0.4,
675                "tandem:hue:claude".to_string(),
676                None,
677            )
678            .unwrap();
679
680        // Search by resonance
681        let results = manager.find_by_resonance(
682            "error handling in async code",
683            &["async".to_string(), "error".to_string()],
684            MemoryType::Technical,
685            0.3, // threshold
686            10,
687        );
688
689        // Should find the Rust memory with higher resonance
690        assert!(!results.is_empty());
691        assert!(results[0].0.content.contains("Rust") || results[0].0.content.contains("error"));
692    }
693
694    #[test]
695    fn test_persistence() {
696        let dir = tempdir().unwrap();
697
698        // Create and save
699        {
700            let mut manager = WaveMemoryManager::new_test(Some(dir.path()));
701            manager
702                .anchor(
703                    "Aye loves Elvis!".to_string(),
704                    vec!["aye".to_string(), "elvis".to_string()],
705                    MemoryType::Joke,
706                    1.0,
707                    1.0,
708                    "tandem:hue:claude".to_string(),
709                    None,
710                )
711                .unwrap();
712            manager.save().unwrap();
713        }
714
715        // Load and verify
716        {
717            let mut manager = WaveMemoryManager::new_test(Some(dir.path()));
718            let results = manager.find_by_keywords(&["elvis".to_string()], 10);
719            assert_eq!(results.len(), 1);
720            assert!(results[0].content.contains("Elvis"));
721        }
722    }
723
724    #[test]
725    fn test_memory_types_to_frequencies() {
726        assert!(MemoryType::Pattern.frequency() < MemoryType::Solution.frequency());
727        assert!(MemoryType::Solution.frequency() < MemoryType::Technical.frequency());
728        assert!(MemoryType::Technical.frequency() < MemoryType::Joke.frequency());
729    }
730}