Skip to main content

hanzo_mcp/tools/
memory_tool.rs

1/// Memory and knowledge management tool (HIP-0300)
2///
3/// Provides persistent memory capabilities:
4/// - recall: Search memories
5/// - create: Store new memories
6/// - update: Update existing memories
7/// - delete: Remove memories
8/// - facts: Manage knowledge base facts
9/// - summarize: Summarize and store information
10/// - kb: Knowledge base management
11/// - help: Documentation
12///
13/// Persistence: ~/.hanzo/memory.json (cross-runtime compatible with TypeScript)
14
15use anyhow::{anyhow, Result};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use std::collections::HashMap;
19use std::path::PathBuf;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22
23// ---------------------------------------------------------------------------
24// Types
25// ---------------------------------------------------------------------------
26
27/// Memory scope
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(rename_all = "snake_case")]
30pub enum MemoryScope {
31    Session,
32    Project,
33    Global,
34}
35
36impl Default for MemoryScope {
37    fn default() -> Self {
38        Self::Project
39    }
40}
41
42impl std::str::FromStr for MemoryScope {
43    type Err = anyhow::Error;
44
45    fn from_str(s: &str) -> Result<Self> {
46        match s.to_lowercase().as_str() {
47            "session" => Ok(Self::Session),
48            "project" => Ok(Self::Project),
49            "global" => Ok(Self::Global),
50            _ => Ok(Self::Project),
51        }
52    }
53}
54
55impl std::fmt::Display for MemoryScope {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::Session => write!(f, "session"),
59            Self::Project => write!(f, "project"),
60            Self::Global => write!(f, "global"),
61        }
62    }
63}
64
65/// Memory action types
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67#[serde(rename_all = "snake_case")]
68pub enum MemoryAction {
69    Recall,
70    Create,
71    Update,
72    Delete,
73    Manage,
74    Facts,
75    Summarize,
76    List,
77    Stats,
78    Clear,
79    Export,
80    Import,
81    Merge,
82    Tag,
83    Untag,
84    Namespaces,
85    History,
86    Kb,
87    Help,
88}
89
90impl Default for MemoryAction {
91    fn default() -> Self {
92        Self::Help
93    }
94}
95
96impl std::str::FromStr for MemoryAction {
97    type Err = anyhow::Error;
98
99    fn from_str(s: &str) -> Result<Self> {
100        match s.to_lowercase().as_str() {
101            "recall" | "search" | "query" => Ok(Self::Recall),
102            "create" | "add" | "store" => Ok(Self::Create),
103            "update" | "modify" => Ok(Self::Update),
104            "delete" | "remove" => Ok(Self::Delete),
105            "manage" => Ok(Self::Manage),
106            "facts" | "fact" => Ok(Self::Facts),
107            "summarize" | "summary" => Ok(Self::Summarize),
108            "list" => Ok(Self::List),
109            "stats" => Ok(Self::Stats),
110            "clear" => Ok(Self::Clear),
111            "export" => Ok(Self::Export),
112            "import" | "import_memories" => Ok(Self::Import),
113            "merge" => Ok(Self::Merge),
114            "tag" => Ok(Self::Tag),
115            "untag" => Ok(Self::Untag),
116            "namespaces" => Ok(Self::Namespaces),
117            "history" => Ok(Self::History),
118            "kb" => Ok(Self::Kb),
119            "help" | "" => Ok(Self::Help),
120            _ => Err(anyhow!("Unknown action: {}", s)),
121        }
122    }
123}
124
125/// A stored memory
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Memory {
128    pub id: String,
129    pub content: String,
130    pub scope: MemoryScope,
131    pub created_at: String,
132    pub updated_at: String,
133    pub metadata: HashMap<String, Value>,
134}
135
136/// A fact in a knowledge base
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Fact {
139    pub id: String,
140    pub content: String,
141    pub kb_name: String,
142    pub scope: MemoryScope,
143    pub created_at: String,
144}
145
146/// Knowledge base
147#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148pub struct KnowledgeBase {
149    pub name: String,
150    pub description: Option<String>,
151    pub scope: MemoryScope,
152    pub facts: Vec<Fact>,
153    pub created_at: String,
154}
155
156// ---------------------------------------------------------------------------
157// Persistence format — matches TypeScript ~/.hanzo/memory.json
158// ---------------------------------------------------------------------------
159
160/// On-disk entry format (TypeScript-compatible)
161#[derive(Debug, Clone, Serialize, Deserialize)]
162struct PersistEntry {
163    id: String,
164    key: String,
165    value: String,
166    tags: Vec<String>,
167    namespace: String,
168    created: String,
169    updated: String,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    metadata: Option<HashMap<String, Value>>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    ttl: Option<String>,
174}
175
176/// On-disk fact format
177#[derive(Debug, Clone, Serialize, Deserialize)]
178struct PersistFact {
179    id: String,
180    content: String,
181    kb_name: String,
182    scope: String,
183    created: String,
184}
185
186/// On-disk KB metadata
187#[derive(Debug, Clone, Serialize, Deserialize)]
188struct PersistKb {
189    name: String,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    description: Option<String>,
192    scope: String,
193    created: String,
194}
195
196/// On-disk store format (TypeScript-compatible)
197#[derive(Debug, Clone, Serialize, Deserialize)]
198struct PersistStore {
199    entries: Vec<PersistEntry>,
200    #[serde(rename = "lastId")]
201    last_id: u64,
202    #[serde(default)]
203    facts: Vec<PersistFact>,
204    #[serde(rename = "lastFactId", default)]
205    last_fact_id: u64,
206    #[serde(rename = "knowledgeBases", default)]
207    knowledge_bases: Vec<PersistKb>,
208}
209
210impl Default for PersistStore {
211    fn default() -> Self {
212        Self {
213            entries: Vec::new(),
214            last_id: 0,
215            facts: Vec::new(),
216            last_fact_id: 0,
217            knowledge_bases: Vec::new(),
218        }
219    }
220}
221
222// ---------------------------------------------------------------------------
223// Conversion: Memory <-> PersistEntry
224// ---------------------------------------------------------------------------
225
226fn scope_to_namespace(scope: &MemoryScope, metadata: &HashMap<String, Value>) -> String {
227    if let Some(Value::String(ns)) = metadata.get("namespace") {
228        return ns.clone();
229    }
230    match scope {
231        MemoryScope::Session => "session".to_string(),
232        MemoryScope::Project => "default".to_string(),
233        MemoryScope::Global => "global".to_string(),
234    }
235}
236
237fn namespace_to_scope(ns: &str) -> MemoryScope {
238    match ns {
239        "session" => MemoryScope::Session,
240        "global" => MemoryScope::Global,
241        "default" => MemoryScope::Project,
242        _ => MemoryScope::Project,
243    }
244}
245
246fn memory_to_entry(m: &Memory) -> PersistEntry {
247    let tags: Vec<String> = m.metadata.keys()
248        .filter(|k| k.starts_with("tag:"))
249        .map(|k| k.strip_prefix("tag:").unwrap().to_string())
250        .collect();
251
252    let key = m.metadata.get("key")
253        .and_then(|v| v.as_str())
254        .unwrap_or(&m.id)
255        .to_string();
256
257    let ttl = m.metadata.get("ttl")
258        .and_then(|v| v.as_str())
259        .map(|s| s.to_string());
260
261    // Build clean metadata (exclude tags, key, ttl, namespace which are stored in dedicated fields)
262    let clean_meta: HashMap<String, Value> = m.metadata.iter()
263        .filter(|(k, _)| !k.starts_with("tag:") && *k != "key" && *k != "ttl" && *k != "namespace")
264        .map(|(k, v)| (k.clone(), v.clone()))
265        .collect();
266
267    PersistEntry {
268        id: m.id.clone(),
269        key,
270        value: m.content.clone(),
271        tags,
272        namespace: scope_to_namespace(&m.scope, &m.metadata),
273        created: m.created_at.clone(),
274        updated: m.updated_at.clone(),
275        metadata: if clean_meta.is_empty() { None } else { Some(clean_meta) },
276        ttl,
277    }
278}
279
280fn entry_to_memory(e: &PersistEntry) -> Memory {
281    let mut metadata: HashMap<String, Value> = e.metadata.clone().unwrap_or_default();
282
283    // Store tags in metadata
284    for tag in &e.tags {
285        metadata.insert(format!("tag:{}", tag), json!(true));
286    }
287
288    // Store key in metadata
289    metadata.insert("key".to_string(), json!(e.key));
290
291    // Store TTL in metadata
292    if let Some(ref ttl) = e.ttl {
293        metadata.insert("ttl".to_string(), json!(ttl));
294    }
295
296    // Store namespace in metadata if not a standard scope name
297    let scope = namespace_to_scope(&e.namespace);
298    if e.namespace != "session" && e.namespace != "global" && e.namespace != "default" {
299        metadata.insert("namespace".to_string(), json!(e.namespace));
300    }
301
302    Memory {
303        id: e.id.clone(),
304        content: e.value.clone(),
305        scope,
306        created_at: e.created.clone(),
307        updated_at: e.updated.clone(),
308        metadata,
309    }
310}
311
312fn fact_to_persist(f: &Fact) -> PersistFact {
313    PersistFact {
314        id: f.id.clone(),
315        content: f.content.clone(),
316        kb_name: f.kb_name.clone(),
317        scope: f.scope.to_string(),
318        created: f.created_at.clone(),
319    }
320}
321
322fn persist_to_fact(p: &PersistFact) -> Fact {
323    Fact {
324        id: p.id.clone(),
325        content: p.content.clone(),
326        kb_name: p.kb_name.clone(),
327        scope: p.scope.parse().unwrap_or_default(),
328        created_at: p.created.clone(),
329    }
330}
331
332// ---------------------------------------------------------------------------
333// TTL check
334// ---------------------------------------------------------------------------
335
336fn is_expired(metadata: &HashMap<String, Value>) -> bool {
337    if let Some(Value::String(ttl)) = metadata.get("ttl") {
338        if let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(ttl) {
339            return expiry < chrono::Utc::now();
340        }
341        // Try ISO date without timezone
342        if let Ok(expiry) = chrono::NaiveDateTime::parse_from_str(ttl, "%Y-%m-%dT%H:%M:%S") {
343            return expiry < chrono::Utc::now().naive_utc();
344        }
345    }
346    false
347}
348
349fn entry_is_expired(e: &PersistEntry) -> bool {
350    if let Some(ref ttl) = e.ttl {
351        if let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(ttl) {
352            return expiry < chrono::Utc::now();
353        }
354        if let Ok(expiry) = chrono::NaiveDateTime::parse_from_str(ttl, "%Y-%m-%dT%H:%M:%S") {
355            return expiry < chrono::Utc::now().naive_utc();
356        }
357    }
358    false
359}
360
361// ---------------------------------------------------------------------------
362// Arguments
363// ---------------------------------------------------------------------------
364
365/// Arguments for memory tool
366#[derive(Debug, Clone, Default, Serialize, Deserialize)]
367pub struct MemoryToolArgs {
368    #[serde(default)]
369    pub action: String,
370    /// Query for recall
371    pub queries: Option<Vec<String>>,
372    /// Single query
373    pub query: Option<String>,
374    /// Statements to store
375    pub statements: Option<Vec<String>>,
376    /// Single statement
377    pub statement: Option<String>,
378    /// Memory ID for update/delete
379    pub id: Option<String>,
380    /// Memory IDs for batch operations
381    pub ids: Option<Vec<String>>,
382    /// Updates for batch update
383    pub updates: Option<Vec<Value>>,
384    /// Scope
385    pub scope: Option<String>,
386    /// Namespace (alias for scope)
387    pub namespace: Option<String>,
388    /// Key for key-based storage
389    pub key: Option<String>,
390    /// TTL as ISO date string
391    pub ttl: Option<String>,
392    /// Limit results
393    pub limit: Option<usize>,
394    /// Knowledge base name
395    pub kb_name: Option<String>,
396    /// Facts to store
397    pub facts: Option<Vec<String>>,
398    /// Content to summarize
399    pub content: Option<String>,
400    /// Topic for summary
401    pub topic: Option<String>,
402    /// Metadata
403    pub metadata: Option<HashMap<String, Value>>,
404    /// Creations for manage
405    pub creations: Option<Vec<String>>,
406    /// Deletions for manage
407    pub deletions: Option<Vec<String>>,
408    /// Tag name for tag/untag
409    pub tag: Option<String>,
410    /// JSON data for import
411    pub data: Option<String>,
412    /// Description for kb create
413    pub description: Option<String>,
414    /// Sub-action for kb management
415    pub sub_action: Option<String>,
416}
417
418// ---------------------------------------------------------------------------
419// MemoryTool
420// ---------------------------------------------------------------------------
421
422/// Memory tool
423pub struct MemoryTool {
424    memories: Arc<RwLock<HashMap<String, Memory>>>,
425    knowledge_bases: Arc<RwLock<HashMap<String, KnowledgeBase>>>,
426    counter: Arc<RwLock<u64>>,
427    fact_counter: Arc<RwLock<u64>>,
428    history: Arc<RwLock<Vec<String>>>,
429    loaded: Arc<RwLock<bool>>,
430    storage_path: PathBuf,
431}
432
433impl MemoryTool {
434    pub fn new() -> Self {
435        Self::with_path(None)
436    }
437
438    /// Create with explicit storage path (useful for testing)
439    pub fn with_path(path: Option<PathBuf>) -> Self {
440        let storage_path = path.unwrap_or_else(|| {
441            if let Ok(p) = std::env::var("MEMORY_PATH") {
442                PathBuf::from(p)
443            } else {
444                dirs::home_dir()
445                    .unwrap_or_else(|| PathBuf::from("."))
446                    .join(".hanzo")
447                    .join("memory.json")
448            }
449        });
450
451        let tool = Self {
452            memories: Arc::new(RwLock::new(HashMap::new())),
453            knowledge_bases: Arc::new(RwLock::new(HashMap::new())),
454            counter: Arc::new(RwLock::new(0)),
455            fact_counter: Arc::new(RwLock::new(0)),
456            history: Arc::new(RwLock::new(Vec::new())),
457            loaded: Arc::new(RwLock::new(false)),
458            storage_path,
459        };
460
461        // Load is async so we can't do it in the constructor.
462        // The first execute() call will trigger load if needed.
463        tool
464    }
465
466    /// Load from disk. Called lazily on first execute.
467    async fn ensure_loaded(&self) -> Result<()> {
468        {
469            let loaded = self.loaded.read().await;
470            if *loaded {
471                return Ok(());
472            }
473        }
474        self.load_from_disk().await?;
475        *self.loaded.write().await = true;
476        Ok(())
477    }
478
479    async fn load_from_disk(&self) -> Result<()> {
480        let store = match tokio::fs::read_to_string(&self.storage_path).await {
481            Ok(data) => {
482                serde_json::from_str::<PersistStore>(&data).unwrap_or_default()
483            }
484            Err(_) => PersistStore::default(),
485        };
486
487        let mut memories = self.memories.write().await;
488        let mut kbs = self.knowledge_bases.write().await;
489        let mut counter = self.counter.write().await;
490        let mut fact_counter = self.fact_counter.write().await;
491
492        memories.clear();
493        kbs.clear();
494
495        *counter = store.last_id;
496        *fact_counter = store.last_fact_id;
497
498        // Load entries, filtering expired
499        for entry in &store.entries {
500            if entry_is_expired(entry) {
501                continue;
502            }
503            let memory = entry_to_memory(entry);
504            memories.insert(memory.id.clone(), memory);
505        }
506
507        // Load KB metadata first (so empty KBs are preserved)
508        for pkb in &store.knowledge_bases {
509            kbs.entry(pkb.name.clone()).or_insert_with(|| KnowledgeBase {
510                name: pkb.name.clone(),
511                description: pkb.description.clone(),
512                scope: pkb.scope.parse().unwrap_or_default(),
513                facts: Vec::new(),
514                created_at: pkb.created.clone(),
515            });
516        }
517
518        // Load facts into knowledge bases
519        for pf in &store.facts {
520            let fact = persist_to_fact(pf);
521            let kb = kbs.entry(fact.kb_name.clone()).or_insert_with(|| KnowledgeBase {
522                name: fact.kb_name.clone(),
523                description: None,
524                scope: fact.scope.clone(),
525                facts: Vec::new(),
526                created_at: fact.created_at.clone(),
527            });
528            kb.facts.push(fact);
529        }
530
531        Ok(())
532    }
533
534    async fn save_to_disk(&self) -> Result<()> {
535        let memories = self.memories.read().await;
536        let kbs = self.knowledge_bases.read().await;
537        let counter = self.counter.read().await;
538        let fact_counter = self.fact_counter.read().await;
539
540        // Build entries, filtering expired
541        let entries: Vec<PersistEntry> = memories.values()
542            .filter(|m| !is_expired(&m.metadata))
543            .map(|m| memory_to_entry(m))
544            .collect();
545
546        // Build facts from knowledge bases
547        let facts: Vec<PersistFact> = kbs.values()
548            .flat_map(|kb| kb.facts.iter().map(|f| fact_to_persist(f)))
549            .collect();
550
551        // Build KB metadata
552        let knowledge_bases: Vec<PersistKb> = kbs.values()
553            .map(|kb| PersistKb {
554                name: kb.name.clone(),
555                description: kb.description.clone(),
556                scope: kb.scope.to_string(),
557                created: kb.created_at.clone(),
558            })
559            .collect();
560
561        let store = PersistStore {
562            entries,
563            last_id: *counter,
564            facts,
565            last_fact_id: *fact_counter,
566            knowledge_bases,
567        };
568
569        // Ensure parent directory exists
570        if let Some(parent) = self.storage_path.parent() {
571            tokio::fs::create_dir_all(parent).await?;
572        }
573
574        let json = serde_json::to_string_pretty(&store)?;
575        tokio::fs::write(&self.storage_path, json).await?;
576        Ok(())
577    }
578
579    async fn next_id(&self, prefix: &str) -> String {
580        let mut counter = self.counter.write().await;
581        *counter += 1;
582        format!("{}_{}", prefix, *counter)
583    }
584
585    async fn next_fact_id(&self) -> String {
586        let mut counter = self.fact_counter.write().await;
587        *counter += 1;
588        format!("fact_{}", *counter)
589    }
590
591    /// Resolve scope from args: namespace takes priority, then scope, then default
592    fn resolve_scope(args: &MemoryToolArgs) -> (MemoryScope, Option<String>) {
593        if let Some(ref ns) = args.namespace {
594            match ns.as_str() {
595                "default" => (MemoryScope::Project, None),
596                "session" => (MemoryScope::Session, None),
597                "global" => (MemoryScope::Global, None),
598                other => (MemoryScope::Project, Some(other.to_string())),
599            }
600        } else if let Some(ref s) = args.scope {
601            (s.parse().unwrap_or_default(), None)
602        } else {
603            (MemoryScope::Project, None)
604        }
605    }
606
607    /// Build metadata including key, ttl, namespace overrides
608    fn build_metadata(args: &MemoryToolArgs, custom_namespace: &Option<String>) -> HashMap<String, Value> {
609        let mut metadata = args.metadata.clone().unwrap_or_default();
610        if let Some(ref key) = args.key {
611            metadata.insert("key".to_string(), json!(key));
612        }
613        if let Some(ref ttl) = args.ttl {
614            metadata.insert("ttl".to_string(), json!(ttl));
615        }
616        if let Some(ref ns) = custom_namespace {
617            metadata.insert("namespace".to_string(), json!(ns));
618        }
619        metadata
620    }
621
622    pub async fn execute(&self, args: MemoryToolArgs) -> Result<String> {
623        // Ensure data is loaded from disk on first call
624        self.ensure_loaded().await?;
625
626        let action: MemoryAction = if args.action.is_empty() {
627            MemoryAction::Help
628        } else {
629            args.action.parse()?
630        };
631
632        let result = match action {
633            MemoryAction::Recall => self.recall(args).await?,
634            MemoryAction::Create => self.create(args).await?,
635            MemoryAction::Update => self.update(args).await?,
636            MemoryAction::Delete => self.delete(args).await?,
637            MemoryAction::Manage => self.manage(args).await?,
638            MemoryAction::Facts => self.facts(args).await?,
639            MemoryAction::Summarize => self.summarize(args).await?,
640            MemoryAction::List => self.list(args).await?,
641            MemoryAction::Stats => self.stats(args).await?,
642            MemoryAction::Clear => self.clear(args).await?,
643            MemoryAction::Export => self.export_memories().await?,
644            MemoryAction::Import => self.import_memories(args).await?,
645            MemoryAction::Merge => self.merge_memories().await?,
646            MemoryAction::Tag => self.tag_memory(args).await?,
647            MemoryAction::Untag => self.untag_memory(args).await?,
648            MemoryAction::Namespaces => self.namespaces().await?,
649            MemoryAction::History => self.history_log().await?,
650            MemoryAction::Kb => self.kb(args).await?,
651            MemoryAction::Help => self.help()?,
652        };
653
654        Ok(serde_json::to_string(&result)?)
655    }
656
657    // -----------------------------------------------------------------------
658    // Actions
659    // -----------------------------------------------------------------------
660
661    async fn recall(&self, args: MemoryToolArgs) -> Result<Value> {
662        let queries = args.queries.clone()
663            .or_else(|| args.query.clone().map(|q| vec![q]))
664            .unwrap_or_default();
665        let (scope, custom_ns) = Self::resolve_scope(&args);
666        let limit = args.limit.unwrap_or(10);
667
668        let memories = self.memories.read().await;
669        let mut results = Vec::new();
670
671        // Key-based recall: if key is provided, filter by key
672        if let Some(ref key) = args.key {
673            let matches: Vec<&Memory> = memories.values()
674                .filter(|m| {
675                    !is_expired(&m.metadata)
676                        && m.metadata.get("key").and_then(|v| v.as_str()) == Some(key.as_str())
677                        && Self::matches_scope_and_ns(m, &scope, &custom_ns)
678                })
679                .take(limit)
680                .collect();
681
682            for m in matches {
683                results.push(memory_to_json(m));
684            }
685
686            return Ok(json!({
687                "key": key,
688                "scope": scope.to_string(),
689                "results": results,
690                "count": results.len()
691            }));
692        }
693
694        // Query-based recall
695        if queries.is_empty() {
696            // No queries and no key — return recent memories in scope
697            let matches: Vec<&Memory> = memories.values()
698                .filter(|m| {
699                    !is_expired(&m.metadata)
700                        && Self::matches_scope_and_ns(m, &scope, &custom_ns)
701                })
702                .take(limit)
703                .collect();
704            for m in matches {
705                results.push(memory_to_json(m));
706            }
707        } else {
708            for query in &queries {
709                let query_lower = query.to_lowercase();
710                let terms: Vec<&str> = query_lower.split_whitespace().collect();
711                let matches: Vec<&Memory> = memories.values()
712                    .filter(|m| {
713                        if is_expired(&m.metadata) { return false; }
714                        if !Self::matches_scope_and_ns(m, &scope, &custom_ns) { return false; }
715                        let text = m.content.to_lowercase();
716                        terms.iter().all(|t| text.contains(t))
717                    })
718                    .take(limit)
719                    .collect();
720
721                for m in matches {
722                    results.push(memory_to_json(m));
723                }
724            }
725        }
726
727        Ok(json!({
728            "queries": queries,
729            "scope": scope.to_string(),
730            "results": results,
731            "count": results.len()
732        }))
733    }
734
735    async fn create(&self, args: MemoryToolArgs) -> Result<Value> {
736        let statements = args.statements.clone()
737            .or_else(|| args.statement.clone().map(|s| vec![s]))
738            .or_else(|| args.content.clone().map(|c| vec![c]))
739            .ok_or_else(|| anyhow!("statements required"))?;
740        let (scope, custom_ns) = Self::resolve_scope(&args);
741        let metadata = Self::build_metadata(&args, &custom_ns);
742        let now = chrono::Utc::now().to_rfc3339();
743
744        let mut created_ids = Vec::new();
745        let mut memories = self.memories.write().await;
746
747        for statement in statements {
748            let id = self.next_id("mem").await;
749            let memory = Memory {
750                id: id.clone(),
751                content: statement,
752                scope: scope.clone(),
753                created_at: now.clone(),
754                updated_at: now.clone(),
755                metadata: metadata.clone(),
756            };
757            memories.insert(id.clone(), memory);
758            created_ids.push(id);
759        }
760
761        drop(memories);
762        self.save_to_disk().await?;
763        self.record_history(&format!("create: {} memories", created_ids.len())).await;
764
765        Ok(json!({
766            "created": created_ids.len(),
767            "ids": created_ids,
768            "scope": scope.to_string()
769        }))
770    }
771
772    async fn update(&self, args: MemoryToolArgs) -> Result<Value> {
773        let now = chrono::Utc::now().to_rfc3339();
774        let mut updated_ids = Vec::new();
775
776        // Single update by id + content/statement
777        if let Some(ref id) = args.id {
778            let new_content = args.statement.as_deref()
779                .or(args.content.as_deref());
780            if let Some(content) = new_content {
781                let mut memories = self.memories.write().await;
782                if let Some(memory) = memories.get_mut(id.as_str()) {
783                    memory.content = content.to_string();
784                    memory.updated_at = now.clone();
785                    if let Some(ref ttl) = args.ttl {
786                        memory.metadata.insert("ttl".to_string(), json!(ttl));
787                    }
788                    if let Some(ref key) = args.key {
789                        memory.metadata.insert("key".to_string(), json!(key));
790                    }
791                    updated_ids.push(id.clone());
792                }
793                drop(memories);
794                if !updated_ids.is_empty() {
795                    self.save_to_disk().await?;
796                }
797                return Ok(json!({ "updated": updated_ids.len(), "ids": updated_ids }));
798            }
799        }
800
801        // Batch updates
802        let updates = args.updates.ok_or_else(|| anyhow!("updates or (id + statement) required"))?;
803        let mut memories = self.memories.write().await;
804
805        for update_val in updates {
806            if let Some(obj) = update_val.as_object() {
807                if let (Some(id), Some(statement)) = (
808                    obj.get("id").and_then(|v| v.as_str()),
809                    obj.get("statement").and_then(|v| v.as_str())
810                ) {
811                    if let Some(memory) = memories.get_mut(id) {
812                        memory.content = statement.to_string();
813                        memory.updated_at = now.clone();
814                        updated_ids.push(id.to_string());
815                    }
816                }
817            }
818        }
819
820        drop(memories);
821        if !updated_ids.is_empty() {
822            self.save_to_disk().await?;
823        }
824        self.record_history(&format!("update: {} memories", updated_ids.len())).await;
825
826        Ok(json!({
827            "updated": updated_ids.len(),
828            "ids": updated_ids
829        }))
830    }
831
832    async fn delete(&self, args: MemoryToolArgs) -> Result<Value> {
833        let mut ids_to_delete = args.ids.clone()
834            .or_else(|| args.id.clone().map(|id| vec![id]))
835            .unwrap_or_default();
836
837        // Delete by key
838        if ids_to_delete.is_empty() {
839            if let Some(ref key) = args.key {
840                let memories = self.memories.read().await;
841                ids_to_delete = memories.values()
842                    .filter(|m| m.metadata.get("key").and_then(|v| v.as_str()) == Some(key.as_str()))
843                    .map(|m| m.id.clone())
844                    .collect();
845            }
846        }
847
848        if ids_to_delete.is_empty() {
849            return Err(anyhow!("ids, id, or key required"));
850        }
851
852        let mut deleted_ids = Vec::new();
853        let mut memories = self.memories.write().await;
854
855        for id in ids_to_delete {
856            if memories.remove(&id).is_some() {
857                deleted_ids.push(id);
858            }
859        }
860
861        drop(memories);
862        if !deleted_ids.is_empty() {
863            self.save_to_disk().await?;
864        }
865        self.record_history(&format!("delete: {} memories", deleted_ids.len())).await;
866
867        Ok(json!({
868            "deleted": deleted_ids.len(),
869            "ids": deleted_ids
870        }))
871    }
872
873    async fn manage(&self, args: MemoryToolArgs) -> Result<Value> {
874        let (scope, custom_ns) = Self::resolve_scope(&args);
875        let metadata = Self::build_metadata(&args, &custom_ns);
876        let now = chrono::Utc::now().to_rfc3339();
877
878        let mut created_ids = Vec::new();
879        let mut updated_ids = Vec::new();
880        let mut deleted_ids = Vec::new();
881
882        // Handle creations
883        if let Some(creations) = args.creations.clone() {
884            let mut memories = self.memories.write().await;
885            for statement in creations {
886                let id = self.next_id("mem").await;
887                let memory = Memory {
888                    id: id.clone(),
889                    content: statement,
890                    scope: scope.clone(),
891                    created_at: now.clone(),
892                    updated_at: now.clone(),
893                    metadata: metadata.clone(),
894                };
895                memories.insert(id.clone(), memory);
896                created_ids.push(id);
897            }
898        }
899
900        // Handle updates
901        if let Some(updates) = args.updates.clone() {
902            let mut memories = self.memories.write().await;
903            for update_val in updates {
904                if let Some(obj) = update_val.as_object() {
905                    if let (Some(id), Some(statement)) = (
906                        obj.get("id").and_then(|v| v.as_str()),
907                        obj.get("statement").and_then(|v| v.as_str())
908                    ) {
909                        if let Some(memory) = memories.get_mut(id) {
910                            memory.content = statement.to_string();
911                            memory.updated_at = now.clone();
912                            updated_ids.push(id.to_string());
913                        }
914                    }
915                }
916            }
917        }
918
919        // Handle deletions
920        if let Some(deletions) = args.deletions.clone() {
921            let mut memories = self.memories.write().await;
922            for id in deletions {
923                if memories.remove(&id).is_some() {
924                    deleted_ids.push(id);
925                }
926            }
927        }
928
929        let mutated = !created_ids.is_empty() || !updated_ids.is_empty() || !deleted_ids.is_empty();
930        if mutated {
931            self.save_to_disk().await?;
932        }
933
934        Ok(json!({
935            "created": created_ids,
936            "updated": updated_ids,
937            "deleted": deleted_ids,
938            "scope": scope.to_string()
939        }))
940    }
941
942    async fn facts(&self, args: MemoryToolArgs) -> Result<Value> {
943        let kb_name = args.kb_name.clone().unwrap_or_else(|| "general".to_string());
944        let (scope, _custom_ns) = Self::resolve_scope(&args);
945        let now = chrono::Utc::now().to_rfc3339();
946
947        if let Some(new_facts) = args.facts.clone() {
948            // Store facts
949            let mut kbs = self.knowledge_bases.write().await;
950            let kb = kbs.entry(kb_name.clone()).or_insert_with(|| KnowledgeBase {
951                name: kb_name.clone(),
952                description: None,
953                scope: scope.clone(),
954                facts: Vec::new(),
955                created_at: now.clone(),
956            });
957
958            let mut created_ids = Vec::new();
959            for fact_content in new_facts {
960                let id = self.next_fact_id().await;
961                let fact = Fact {
962                    id: id.clone(),
963                    content: fact_content,
964                    kb_name: kb_name.clone(),
965                    scope: scope.clone(),
966                    created_at: now.clone(),
967                };
968                kb.facts.push(fact);
969                created_ids.push(id);
970            }
971
972            drop(kbs);
973            self.save_to_disk().await?;
974
975            return Ok(json!({
976                "stored": created_ids.len(),
977                "ids": created_ids,
978                "kb_name": kb_name
979            }));
980        }
981
982        // Recall facts
983        if let Some(queries) = args.queries.clone().or_else(|| args.query.clone().map(|q| vec![q])) {
984            let kbs = self.knowledge_bases.read().await;
985            let limit = args.limit.unwrap_or(10);
986            let mut results = Vec::new();
987
988            if let Some(kb) = kbs.get(&kb_name) {
989                for query in &queries {
990                    let query_lower = query.to_lowercase();
991                    let matches: Vec<&Fact> = kb.facts.iter()
992                        .filter(|f| f.content.to_lowercase().contains(&query_lower))
993                        .take(limit)
994                        .collect();
995
996                    for f in matches {
997                        results.push(json!({
998                            "id": f.id,
999                            "content": f.content,
1000                            "kb_name": f.kb_name
1001                        }));
1002                    }
1003                }
1004            }
1005
1006            return Ok(json!({
1007                "queries": queries,
1008                "kb_name": kb_name,
1009                "results": results,
1010                "count": results.len()
1011            }));
1012        }
1013
1014        // List knowledge bases
1015        let kbs = self.knowledge_bases.read().await;
1016        let kb_list: Vec<Value> = kbs.values()
1017            .map(|kb| json!({
1018                "name": kb.name,
1019                "description": kb.description,
1020                "fact_count": kb.facts.len(),
1021                "scope": kb.scope.to_string()
1022            }))
1023            .collect();
1024
1025        Ok(json!({
1026            "knowledge_bases": kb_list,
1027            "count": kb_list.len()
1028        }))
1029    }
1030
1031    async fn summarize(&self, args: MemoryToolArgs) -> Result<Value> {
1032        let content = args.content.clone().ok_or_else(|| anyhow!("content required"))?;
1033        let topic = args.topic.clone().ok_or_else(|| anyhow!("topic required"))?;
1034        let (scope, custom_ns) = Self::resolve_scope(&args);
1035        let now = chrono::Utc::now().to_rfc3339();
1036
1037        // Create memory from summary
1038        let id = self.next_id("mem").await;
1039        let summary = format!("[{}] {}", topic, content);
1040        let mut metadata = Self::build_metadata(&args, &custom_ns);
1041        metadata.insert("topic".to_string(), json!(topic));
1042        metadata.insert("type".to_string(), json!("summary"));
1043
1044        let memory = Memory {
1045            id: id.clone(),
1046            content: summary.clone(),
1047            scope,
1048            created_at: now.clone(),
1049            updated_at: now,
1050            metadata,
1051        };
1052
1053        self.memories.write().await.insert(id.clone(), memory);
1054        self.save_to_disk().await?;
1055
1056        // Extract key facts (simplified)
1057        let facts: Vec<&str> = content.lines()
1058            .filter(|l| !l.trim().is_empty())
1059            .take(5)
1060            .collect();
1061
1062        Ok(json!({
1063            "id": id,
1064            "topic": topic,
1065            "stored": true,
1066            "extracted_facts": facts.len(),
1067            "facts": facts
1068        }))
1069    }
1070
1071    async fn list(&self, args: MemoryToolArgs) -> Result<Value> {
1072        let (scope, custom_ns) = Self::resolve_scope(&args);
1073        let has_scope_filter = args.scope.is_some() || args.namespace.is_some();
1074        let limit = args.limit.unwrap_or(50);
1075
1076        let memories = self.memories.read().await;
1077        let results: Vec<Value> = memories.values()
1078            .filter(|m| {
1079                if is_expired(&m.metadata) { return false; }
1080                if has_scope_filter {
1081                    Self::matches_scope_and_ns(m, &scope, &custom_ns)
1082                } else {
1083                    true
1084                }
1085            })
1086            .take(limit)
1087            .map(|m| memory_to_json(m))
1088            .collect();
1089
1090        Ok(json!({
1091            "memories": results,
1092            "count": results.len(),
1093            "total": memories.len()
1094        }))
1095    }
1096
1097    async fn record_history(&self, entry: &str) {
1098        let mut history = self.history.write().await;
1099        history.push(format!("[{}] {}", chrono::Utc::now().to_rfc3339(), entry));
1100        if history.len() > 1000 { history.drain(0..500); }
1101    }
1102
1103    async fn stats(&self, _args: MemoryToolArgs) -> Result<Value> {
1104        let memories = self.memories.read().await;
1105        let kbs = self.knowledge_bases.read().await;
1106        let mut by_scope: HashMap<String, usize> = HashMap::new();
1107        let mut by_namespace: HashMap<String, usize> = HashMap::new();
1108        let mut total_size = 0usize;
1109        for m in memories.values() {
1110            if is_expired(&m.metadata) { continue; }
1111            let scope_key = m.scope.to_string();
1112            *by_scope.entry(scope_key).or_insert(0) += 1;
1113            let ns = scope_to_namespace(&m.scope, &m.metadata);
1114            *by_namespace.entry(ns).or_insert(0) += 1;
1115            total_size += m.content.len();
1116        }
1117        Ok(json!({
1118            "total_memories": memories.len(),
1119            "total_size_bytes": total_size,
1120            "by_scope": by_scope,
1121            "by_namespace": by_namespace,
1122            "knowledge_bases": kbs.len(),
1123            "total_facts": kbs.values().map(|kb| kb.facts.len()).sum::<usize>(),
1124            "storage_path": self.storage_path.to_string_lossy()
1125        }))
1126    }
1127
1128    async fn clear(&self, args: MemoryToolArgs) -> Result<Value> {
1129        let (scope, custom_ns) = Self::resolve_scope(&args);
1130        let has_scope_filter = args.scope.is_some() || args.namespace.is_some();
1131        let mut memories = self.memories.write().await;
1132        let before = memories.len();
1133        if has_scope_filter {
1134            memories.retain(|_, m| !Self::matches_scope_and_ns(m, &scope, &custom_ns));
1135        } else {
1136            memories.clear();
1137        }
1138        let cleared = before - memories.len();
1139        drop(memories);
1140        self.save_to_disk().await?;
1141        self.record_history(&format!("clear: removed {} memories", cleared)).await;
1142        Ok(json!({ "cleared": cleared, "remaining": before - cleared }))
1143    }
1144
1145    async fn export_memories(&self) -> Result<Value> {
1146        let memories = self.memories.read().await;
1147        let kbs = self.knowledge_bases.read().await;
1148        let mem_list: Vec<Value> = memories.values()
1149            .filter(|m| !is_expired(&m.metadata))
1150            .map(|m| memory_to_json(m))
1151            .collect();
1152        let kb_list: Vec<Value> = kbs.values().map(|kb| json!({
1153            "name": kb.name, "fact_count": kb.facts.len(),
1154            "scope": kb.scope.to_string()
1155        })).collect();
1156        Ok(json!({
1157            "memories": mem_list,
1158            "knowledge_bases": kb_list,
1159            "count": mem_list.len(),
1160            "storage_path": self.storage_path.to_string_lossy()
1161        }))
1162    }
1163
1164    async fn import_memories(&self, args: MemoryToolArgs) -> Result<Value> {
1165        let data = args.data.clone().ok_or_else(|| anyhow!("data (JSON string) required"))?;
1166        let parsed: Value = serde_json::from_str(&data)?;
1167        let now = chrono::Utc::now().to_rfc3339();
1168        let mut imported = 0;
1169        let mut memories = self.memories.write().await;
1170        if let Some(items) = parsed.get("memories").and_then(|v| v.as_array()) {
1171            for item in items {
1172                let id = self.next_id("mem").await;
1173                let content = item.get("content").and_then(|v| v.as_str())
1174                    .or_else(|| item.get("value").and_then(|v| v.as_str()))
1175                    .unwrap_or("").to_string();
1176                let scope: MemoryScope = item.get("scope").and_then(|v| v.as_str())
1177                    .unwrap_or("project").parse()?;
1178                let mut metadata = HashMap::new();
1179                if let Some(key) = item.get("key").and_then(|v| v.as_str()) {
1180                    metadata.insert("key".to_string(), json!(key));
1181                }
1182                let memory = Memory {
1183                    id: id.clone(), content, scope,
1184                    created_at: now.clone(), updated_at: now.clone(),
1185                    metadata,
1186                };
1187                memories.insert(id, memory);
1188                imported += 1;
1189            }
1190        }
1191        // Also import from "entries" key (TypeScript format)
1192        if let Some(items) = parsed.get("entries").and_then(|v| v.as_array()) {
1193            for item in items {
1194                if let Ok(entry) = serde_json::from_value::<PersistEntry>(item.clone()) {
1195                    if entry_is_expired(&entry) { continue; }
1196                    let memory = entry_to_memory(&entry);
1197                    let id = self.next_id("mem").await;
1198                    let mut m = memory;
1199                    m.id = id.clone();
1200                    memories.insert(id, m);
1201                    imported += 1;
1202                }
1203            }
1204        }
1205        drop(memories);
1206        self.save_to_disk().await?;
1207        self.record_history(&format!("import: {} memories", imported)).await;
1208        Ok(json!({ "imported": imported }))
1209    }
1210
1211    async fn merge_memories(&self) -> Result<Value> {
1212        let mut memories = self.memories.write().await;
1213        let ids: Vec<String> = memories.keys().cloned().collect();
1214        let mut merged = 0;
1215        let mut to_remove = Vec::new();
1216        for i in 0..ids.len() {
1217            for j in (i+1)..ids.len() {
1218                if to_remove.contains(&ids[j]) { continue; }
1219                let a = memories.get(&ids[i]).map(|m| m.content.clone());
1220                let b = memories.get(&ids[j]).map(|m| m.content.clone());
1221                if let (Some(a), Some(b)) = (a, b) {
1222                    if a == b {
1223                        to_remove.push(ids[j].clone());
1224                        merged += 1;
1225                    }
1226                }
1227            }
1228        }
1229        for id in &to_remove { memories.remove(id); }
1230        drop(memories);
1231        if merged > 0 {
1232            self.save_to_disk().await?;
1233        }
1234        self.record_history(&format!("merge: removed {} duplicates", merged)).await;
1235        Ok(json!({ "merged": merged, "removed_ids": to_remove }))
1236    }
1237
1238    async fn tag_memory(&self, args: MemoryToolArgs) -> Result<Value> {
1239        let id = args.id.clone().ok_or_else(|| anyhow!("id required"))?;
1240        let tag = args.tag.clone().ok_or_else(|| anyhow!("tag required"))?;
1241        let mut memories = self.memories.write().await;
1242        let memory = memories.get_mut(&id).ok_or_else(|| anyhow!("Memory not found: {}", id))?;
1243        memory.metadata.insert(format!("tag:{}", tag), json!(true));
1244        drop(memories);
1245        self.save_to_disk().await?;
1246        self.record_history(&format!("tag: {} += {}", id, tag)).await;
1247        Ok(json!({ "id": id, "tag": tag, "tagged": true }))
1248    }
1249
1250    async fn untag_memory(&self, args: MemoryToolArgs) -> Result<Value> {
1251        let id = args.id.clone().ok_or_else(|| anyhow!("id required"))?;
1252        let tag = args.tag.clone().ok_or_else(|| anyhow!("tag required"))?;
1253        let mut memories = self.memories.write().await;
1254        let memory = memories.get_mut(&id).ok_or_else(|| anyhow!("Memory not found: {}", id))?;
1255        memory.metadata.remove(&format!("tag:{}", tag));
1256        drop(memories);
1257        self.save_to_disk().await?;
1258        self.record_history(&format!("untag: {} -= {}", id, tag)).await;
1259        Ok(json!({ "id": id, "tag": tag, "untagged": true }))
1260    }
1261
1262    async fn namespaces(&self) -> Result<Value> {
1263        let memories = self.memories.read().await;
1264        let mut ns_counts: HashMap<String, usize> = HashMap::new();
1265        for m in memories.values() {
1266            if is_expired(&m.metadata) { continue; }
1267            let ns = scope_to_namespace(&m.scope, &m.metadata);
1268            *ns_counts.entry(ns).or_insert(0) += 1;
1269        }
1270        // Also include knowledge base names
1271        let kbs = self.knowledge_bases.read().await;
1272        let kb_names: Vec<String> = kbs.keys().cloned().collect();
1273
1274        Ok(json!({
1275            "namespaces": ns_counts,
1276            "knowledge_bases": kb_names,
1277            "count": ns_counts.len()
1278        }))
1279    }
1280
1281    async fn history_log(&self) -> Result<Value> {
1282        let history = self.history.read().await;
1283        let entries: Vec<&String> = history.iter().rev().take(50).collect();
1284        Ok(json!({ "history": entries, "count": entries.len() }))
1285    }
1286
1287    async fn kb(&self, args: MemoryToolArgs) -> Result<Value> {
1288        let sub = args.sub_action.as_deref()
1289            .or(args.query.as_deref())
1290            .unwrap_or("list");
1291
1292        match sub {
1293            "create" => {
1294                let name = args.kb_name.clone()
1295                    .ok_or_else(|| anyhow!("kb_name required for kb create"))?;
1296                let (scope, _) = Self::resolve_scope(&args);
1297                let now = chrono::Utc::now().to_rfc3339();
1298                let mut kbs = self.knowledge_bases.write().await;
1299                if kbs.contains_key(&name) {
1300                    return Ok(json!({ "error": format!("Knowledge base '{}' already exists", name) }));
1301                }
1302                kbs.insert(name.clone(), KnowledgeBase {
1303                    name: name.clone(),
1304                    description: args.description.clone(),
1305                    scope,
1306                    facts: Vec::new(),
1307                    created_at: now,
1308                });
1309                drop(kbs);
1310                self.save_to_disk().await?;
1311                Ok(json!({ "created": name, "description": args.description }))
1312            }
1313            "delete" => {
1314                let name = args.kb_name.clone()
1315                    .ok_or_else(|| anyhow!("kb_name required for kb delete"))?;
1316                let mut kbs = self.knowledge_bases.write().await;
1317                if kbs.remove(&name).is_some() {
1318                    drop(kbs);
1319                    self.save_to_disk().await?;
1320                    Ok(json!({ "deleted": name }))
1321                } else {
1322                    Ok(json!({ "error": format!("Knowledge base '{}' not found", name) }))
1323                }
1324            }
1325            "list" | _ => {
1326                let kbs = self.knowledge_bases.read().await;
1327                let list: Vec<Value> = kbs.values().map(|kb| json!({
1328                    "name": kb.name,
1329                    "description": kb.description,
1330                    "fact_count": kb.facts.len(),
1331                    "scope": kb.scope.to_string(),
1332                    "created_at": kb.created_at
1333                })).collect();
1334                Ok(json!({ "knowledge_bases": list, "count": list.len() }))
1335            }
1336        }
1337    }
1338
1339    fn help(&self) -> Result<Value> {
1340        Ok(json!({
1341            "name": "memory",
1342            "version": "0.13.0",
1343            "description": "Persistent memory and knowledge management tool (HIP-0300). Data persisted to ~/.hanzo/memory.json, compatible with TypeScript runtime.",
1344            "actions": {
1345                "recall": {
1346                    "description": "Search memories by query or key",
1347                    "params": ["queries|query", "key", "scope|namespace", "limit"]
1348                },
1349                "create": {
1350                    "description": "Store new memories",
1351                    "params": ["statements|statement|content", "scope|namespace", "key", "ttl", "metadata"]
1352                },
1353                "update": {
1354                    "description": "Update existing memories",
1355                    "params": ["id + statement|content", "updates[]", "key", "ttl"]
1356                },
1357                "delete": {
1358                    "description": "Remove memories by id, ids, or key",
1359                    "params": ["id|ids|key"]
1360                },
1361                "manage": {
1362                    "description": "Atomic create/update/delete in one call",
1363                    "params": ["creations[]", "updates[]", "deletions[]", "scope|namespace", "key", "ttl"]
1364                },
1365                "facts": {
1366                    "description": "Store/recall knowledge base facts",
1367                    "params": ["kb_name", "facts[]", "queries|query", "scope", "limit"]
1368                },
1369                "summarize": {
1370                    "description": "Summarize content and store as memory",
1371                    "params": ["content", "topic", "scope|namespace"]
1372                },
1373                "list": {
1374                    "description": "List all memories, optionally filtered by scope/namespace",
1375                    "params": ["scope|namespace", "limit"]
1376                },
1377                "stats": {
1378                    "description": "Memory statistics by scope and namespace",
1379                    "params": []
1380                },
1381                "clear": {
1382                    "description": "Clear memories (optional scope/namespace filter)",
1383                    "params": ["scope|namespace"]
1384                },
1385                "export": {
1386                    "description": "Export all memories as JSON",
1387                    "params": []
1388                },
1389                "import": {
1390                    "description": "Import memories from JSON data string",
1391                    "params": ["data"]
1392                },
1393                "merge": {
1394                    "description": "Remove duplicate memories",
1395                    "params": []
1396                },
1397                "tag": {
1398                    "description": "Add tag to a memory",
1399                    "params": ["id", "tag"]
1400                },
1401                "untag": {
1402                    "description": "Remove tag from a memory",
1403                    "params": ["id", "tag"]
1404                },
1405                "namespaces": {
1406                    "description": "List all namespaces with entry counts",
1407                    "params": []
1408                },
1409                "history": {
1410                    "description": "Recent operation history",
1411                    "params": []
1412                },
1413                "kb": {
1414                    "description": "Knowledge base management (create/list/delete)",
1415                    "params": ["sub_action (create|list|delete)", "kb_name", "description"]
1416                },
1417                "help": {
1418                    "description": "Show this documentation",
1419                    "params": []
1420                }
1421            },
1422            "scopes": ["session", "project", "global"],
1423            "namespace_mapping": {
1424                "default": "project scope",
1425                "session": "session scope",
1426                "global": "global scope",
1427                "<custom>": "project scope with namespace stored in metadata"
1428            },
1429            "persistence": "~/.hanzo/memory.json (auto-loaded, saved after every mutation)"
1430        }))
1431    }
1432
1433    // -----------------------------------------------------------------------
1434    // Helpers
1435    // -----------------------------------------------------------------------
1436
1437    fn matches_scope_and_ns(m: &Memory, scope: &MemoryScope, custom_ns: &Option<String>) -> bool {
1438        if m.scope != *scope {
1439            return false;
1440        }
1441        if let Some(ref ns) = custom_ns {
1442            // Must match the custom namespace in metadata
1443            m.metadata.get("namespace")
1444                .and_then(|v| v.as_str())
1445                .map_or(false, |v| v == ns.as_str())
1446        } else {
1447            // Standard scope — should NOT have a custom namespace
1448            !m.metadata.contains_key("namespace")
1449        }
1450    }
1451}
1452
1453fn memory_to_json(m: &Memory) -> Value {
1454    let ns = scope_to_namespace(&m.scope, &m.metadata);
1455    let key = m.metadata.get("key").and_then(|v| v.as_str()).unwrap_or(&m.id);
1456    let tags: Vec<String> = m.metadata.keys()
1457        .filter(|k| k.starts_with("tag:"))
1458        .map(|k| k.strip_prefix("tag:").unwrap().to_string())
1459        .collect();
1460
1461    json!({
1462        "id": m.id,
1463        "key": key,
1464        "content": m.content,
1465        "scope": m.scope.to_string(),
1466        "namespace": ns,
1467        "tags": tags,
1468        "created_at": m.created_at,
1469        "updated_at": m.updated_at,
1470        "metadata": m.metadata
1471    })
1472}
1473
1474// ---------------------------------------------------------------------------
1475// MCP Tool Definition
1476// ---------------------------------------------------------------------------
1477
1478#[derive(Debug, Serialize, Deserialize)]
1479pub struct MemoryToolDefinition {
1480    pub name: String,
1481    pub description: String,
1482    pub input_schema: Value,
1483}
1484
1485impl MemoryToolDefinition {
1486    pub fn new() -> Self {
1487        Self {
1488            name: "memory".to_string(),
1489            description: r#"Persistent memory and knowledge management tool (HIP-0300).
1490Data persisted to ~/.hanzo/memory.json (cross-runtime compatible).
1491
1492Actions:
1493- recall: Search memories by query or key
1494- create: Store new memories (with optional key, ttl, namespace)
1495- update: Update existing memories
1496- delete: Remove memories by id or key
1497- manage: Atomic create/update/delete
1498- facts: Manage knowledge base facts
1499- summarize: Summarize and store information
1500- list: List all memories
1501- stats: Memory statistics
1502- kb: Knowledge base management (create/list/delete)
1503- help: Show all actions and parameters
1504
1505Scopes: session, project, global
1506Namespaces: alias for scope — "default"->project, "session"->session, "global"->global, custom->project+metadata"#.to_string(),
1507            input_schema: json!({
1508                "type": "object",
1509                "properties": {
1510                    "action": {
1511                        "type": "string",
1512                        "enum": ["recall", "create", "update", "delete", "manage", "facts", "summarize", "list", "stats", "clear", "export", "import", "merge", "tag", "untag", "namespaces", "history", "kb", "help"]
1513                    },
1514                    "queries": {"type": "array", "items": {"type": "string"}},
1515                    "query": {"type": "string"},
1516                    "statements": {"type": "array", "items": {"type": "string"}},
1517                    "statement": {"type": "string"},
1518                    "id": {"type": "string"},
1519                    "ids": {"type": "array", "items": {"type": "string"}},
1520                    "updates": {"type": "array", "items": {"type": "object"}},
1521                    "scope": {"type": "string", "enum": ["session", "project", "global"]},
1522                    "namespace": {"type": "string", "description": "Namespace alias for scope. 'default'->project, 'session'->session, 'global'->global, custom->project with namespace in metadata"},
1523                    "key": {"type": "string", "description": "Key for key-based storage and recall"},
1524                    "ttl": {"type": "string", "description": "Expiry as ISO date string (e.g. 2026-03-25T00:00:00Z)"},
1525                    "limit": {"type": "integer"},
1526                    "kb_name": {"type": "string"},
1527                    "facts": {"type": "array", "items": {"type": "string"}},
1528                    "content": {"type": "string"},
1529                    "topic": {"type": "string"},
1530                    "metadata": {"type": "object"},
1531                    "creations": {"type": "array", "items": {"type": "string"}},
1532                    "deletions": {"type": "array", "items": {"type": "string"}},
1533                    "tag": {"type": "string", "description": "Tag name for tag/untag"},
1534                    "data": {"type": "string", "description": "JSON data for import"},
1535                    "description": {"type": "string", "description": "Description for kb create"},
1536                    "sub_action": {"type": "string", "description": "Sub-action for kb (create|list|delete)"}
1537                }
1538            }),
1539        }
1540    }
1541}
1542
1543// ---------------------------------------------------------------------------
1544// Tests
1545// ---------------------------------------------------------------------------
1546
1547#[cfg(test)]
1548mod tests {
1549    use super::*;
1550    use tempfile::NamedTempFile;
1551
1552    fn test_tool() -> (MemoryTool, NamedTempFile) {
1553        let tmp = NamedTempFile::new().unwrap();
1554        let tool = MemoryTool::with_path(Some(tmp.path().to_path_buf()));
1555        (tool, tmp)
1556    }
1557
1558    #[tokio::test]
1559    async fn test_create_memory() {
1560        let (tool, _tmp) = test_tool();
1561        let args = MemoryToolArgs {
1562            action: "create".to_string(),
1563            statements: Some(vec!["User prefers dark mode".to_string()]),
1564            scope: Some("project".to_string()),
1565            ..Default::default()
1566        };
1567
1568        let result = tool.execute(args).await;
1569        assert!(result.is_ok());
1570        let output = result.unwrap();
1571        assert!(output.contains("created"));
1572    }
1573
1574    #[tokio::test]
1575    async fn test_recall_memory() {
1576        let (tool, _tmp) = test_tool();
1577
1578        // Create first
1579        let args = MemoryToolArgs {
1580            action: "create".to_string(),
1581            statements: Some(vec!["User prefers Python".to_string()]),
1582            ..Default::default()
1583        };
1584        tool.execute(args).await.unwrap();
1585
1586        // Recall
1587        let args = MemoryToolArgs {
1588            action: "recall".to_string(),
1589            queries: Some(vec!["Python".to_string()]),
1590            ..Default::default()
1591        };
1592
1593        let result = tool.execute(args).await;
1594        assert!(result.is_ok());
1595        let output = result.unwrap();
1596        assert!(output.contains("Python"));
1597    }
1598
1599    #[tokio::test]
1600    async fn test_facts() {
1601        let (tool, _tmp) = test_tool();
1602        let args = MemoryToolArgs {
1603            action: "facts".to_string(),
1604            kb_name: Some("coding".to_string()),
1605            facts: Some(vec!["Use uv for Python".to_string()]),
1606            ..Default::default()
1607        };
1608
1609        let result = tool.execute(args).await;
1610        assert!(result.is_ok());
1611        let output = result.unwrap();
1612        assert!(output.contains("stored"));
1613    }
1614
1615    #[tokio::test]
1616    async fn test_summarize() {
1617        let (tool, _tmp) = test_tool();
1618        let args = MemoryToolArgs {
1619            action: "summarize".to_string(),
1620            content: Some("Discussion about API design patterns and best practices.".to_string()),
1621            topic: Some("API Design".to_string()),
1622            ..Default::default()
1623        };
1624
1625        let result = tool.execute(args).await;
1626        assert!(result.is_ok());
1627        let output = result.unwrap();
1628        assert!(output.contains("API Design"));
1629    }
1630
1631    #[tokio::test]
1632    async fn test_persistence_survives_reload() {
1633        let tmp = NamedTempFile::new().unwrap();
1634        let path = tmp.path().to_path_buf();
1635
1636        // Create with first tool instance
1637        {
1638            let tool = MemoryTool::with_path(Some(path.clone()));
1639            let args = MemoryToolArgs {
1640                action: "create".to_string(),
1641                statements: Some(vec!["Persistent memory test".to_string()]),
1642                key: Some("test-key".to_string()),
1643                ..Default::default()
1644            };
1645            tool.execute(args).await.unwrap();
1646        }
1647
1648        // Read with second tool instance (simulating restart)
1649        {
1650            let tool = MemoryTool::with_path(Some(path.clone()));
1651            let args = MemoryToolArgs {
1652                action: "recall".to_string(),
1653                key: Some("test-key".to_string()),
1654                ..Default::default()
1655            };
1656            let result = tool.execute(args).await.unwrap();
1657            assert!(result.contains("Persistent memory test"), "Memory should survive restart: {}", result);
1658        }
1659    }
1660
1661    #[tokio::test]
1662    async fn test_key_based_storage_and_recall() {
1663        let (tool, _tmp) = test_tool();
1664
1665        // Create with key
1666        let args = MemoryToolArgs {
1667            action: "create".to_string(),
1668            statements: Some(vec!["Blue agent report for task 42".to_string()]),
1669            key: Some("blue-report-42".to_string()),
1670            ..Default::default()
1671        };
1672        tool.execute(args).await.unwrap();
1673
1674        // Recall by key
1675        let args = MemoryToolArgs {
1676            action: "recall".to_string(),
1677            key: Some("blue-report-42".to_string()),
1678            ..Default::default()
1679        };
1680        let result = tool.execute(args).await.unwrap();
1681        assert!(result.contains("Blue agent report"), "Key recall should work: {}", result);
1682    }
1683
1684    #[tokio::test]
1685    async fn test_namespace_support() {
1686        let (tool, _tmp) = test_tool();
1687
1688        // Create with custom namespace
1689        let args = MemoryToolArgs {
1690            action: "create".to_string(),
1691            statements: Some(vec!["Blue-red coordination data".to_string()]),
1692            namespace: Some("blue-red".to_string()),
1693            ..Default::default()
1694        };
1695        tool.execute(args).await.unwrap();
1696
1697        // Recall with same namespace
1698        let args = MemoryToolArgs {
1699            action: "recall".to_string(),
1700            queries: Some(vec!["coordination".to_string()]),
1701            namespace: Some("blue-red".to_string()),
1702            ..Default::default()
1703        };
1704        let result = tool.execute(args).await.unwrap();
1705        assert!(result.contains("coordination"), "Namespace recall should work: {}", result);
1706
1707        // Recall with different namespace should NOT find it
1708        let args = MemoryToolArgs {
1709            action: "recall".to_string(),
1710            queries: Some(vec!["coordination".to_string()]),
1711            namespace: Some("other".to_string()),
1712            ..Default::default()
1713        };
1714        let result = tool.execute(args).await.unwrap();
1715        let parsed: Value = serde_json::from_str(&result).unwrap();
1716        assert_eq!(parsed["count"], 0, "Different namespace should not find memory");
1717    }
1718
1719    #[tokio::test]
1720    async fn test_ttl_expiry() {
1721        let (tool, _tmp) = test_tool();
1722
1723        // Create with expired TTL
1724        let args = MemoryToolArgs {
1725            action: "create".to_string(),
1726            statements: Some(vec!["Should be expired".to_string()]),
1727            ttl: Some("2020-01-01T00:00:00Z".to_string()),
1728            ..Default::default()
1729        };
1730        tool.execute(args).await.unwrap();
1731
1732        // Create with future TTL
1733        let args = MemoryToolArgs {
1734            action: "create".to_string(),
1735            statements: Some(vec!["Should be visible".to_string()]),
1736            ttl: Some("2099-01-01T00:00:00Z".to_string()),
1737            ..Default::default()
1738        };
1739        tool.execute(args).await.unwrap();
1740
1741        // List should only show non-expired
1742        let args = MemoryToolArgs {
1743            action: "list".to_string(),
1744            ..Default::default()
1745        };
1746        let result = tool.execute(args).await.unwrap();
1747        assert!(result.contains("Should be visible"), "Non-expired should be visible");
1748        assert!(!result.contains("Should be expired"), "Expired should be filtered: {}", result);
1749    }
1750
1751    #[tokio::test]
1752    async fn test_kb_management() {
1753        let (tool, _tmp) = test_tool();
1754
1755        // Create KB
1756        let args = MemoryToolArgs {
1757            action: "kb".to_string(),
1758            sub_action: Some("create".to_string()),
1759            kb_name: Some("test-kb".to_string()),
1760            description: Some("Test knowledge base".to_string()),
1761            ..Default::default()
1762        };
1763        let result = tool.execute(args).await.unwrap();
1764        assert!(result.contains("test-kb"), "KB should be created: {}", result);
1765
1766        // List KBs
1767        let args = MemoryToolArgs {
1768            action: "kb".to_string(),
1769            sub_action: Some("list".to_string()),
1770            ..Default::default()
1771        };
1772        let result = tool.execute(args).await.unwrap();
1773        assert!(result.contains("test-kb"), "KB should be listed: {}", result);
1774
1775        // Delete KB
1776        let args = MemoryToolArgs {
1777            action: "kb".to_string(),
1778            sub_action: Some("delete".to_string()),
1779            kb_name: Some("test-kb".to_string()),
1780            ..Default::default()
1781        };
1782        let result = tool.execute(args).await.unwrap();
1783        assert!(result.contains("test-kb"), "KB should be deleted: {}", result);
1784
1785        // Verify deleted
1786        let args = MemoryToolArgs {
1787            action: "kb".to_string(),
1788            sub_action: Some("list".to_string()),
1789            ..Default::default()
1790        };
1791        let result = tool.execute(args).await.unwrap();
1792        let parsed: Value = serde_json::from_str(&result).unwrap();
1793        assert_eq!(parsed["count"], 0, "KB list should be empty after delete");
1794    }
1795
1796    #[tokio::test]
1797    async fn test_help_action() {
1798        let (tool, _tmp) = test_tool();
1799        let args = MemoryToolArgs {
1800            action: "help".to_string(),
1801            ..Default::default()
1802        };
1803        let result = tool.execute(args).await.unwrap();
1804        assert!(result.contains("recall"));
1805        assert!(result.contains("create"));
1806        assert!(result.contains("kb"));
1807        assert!(result.contains("persistence"));
1808    }
1809
1810    #[tokio::test]
1811    async fn test_delete_by_key() {
1812        let (tool, _tmp) = test_tool();
1813
1814        // Create with key
1815        let args = MemoryToolArgs {
1816            action: "create".to_string(),
1817            statements: Some(vec!["Deletable memory".to_string()]),
1818            key: Some("delete-me".to_string()),
1819            ..Default::default()
1820        };
1821        tool.execute(args).await.unwrap();
1822
1823        // Delete by key
1824        let args = MemoryToolArgs {
1825            action: "delete".to_string(),
1826            key: Some("delete-me".to_string()),
1827            ..Default::default()
1828        };
1829        let result = tool.execute(args).await.unwrap();
1830        assert!(result.contains("\"deleted\":1"), "Should delete by key: {}", result);
1831
1832        // Verify gone
1833        let args = MemoryToolArgs {
1834            action: "recall".to_string(),
1835            key: Some("delete-me".to_string()),
1836            ..Default::default()
1837        };
1838        let result = tool.execute(args).await.unwrap();
1839        let parsed: Value = serde_json::from_str(&result).unwrap();
1840        assert_eq!(parsed["count"], 0, "Deleted memory should not be found");
1841    }
1842
1843    #[tokio::test]
1844    async fn test_json_format_compatibility() {
1845        let tmp = NamedTempFile::new().unwrap();
1846        let path = tmp.path().to_path_buf();
1847
1848        // Create with Rust tool
1849        {
1850            let tool = MemoryTool::with_path(Some(path.clone()));
1851            let args = MemoryToolArgs {
1852                action: "create".to_string(),
1853                statements: Some(vec!["Cross-runtime test".to_string()]),
1854                key: Some("compat-key".to_string()),
1855                namespace: Some("blue-red".to_string()),
1856                ..Default::default()
1857            };
1858            tool.execute(args).await.unwrap();
1859        }
1860
1861        // Read raw JSON and verify TypeScript-compatible format
1862        let data = tokio::fs::read_to_string(&path).await.unwrap();
1863        let store: Value = serde_json::from_str(&data).unwrap();
1864
1865        assert!(store.get("entries").is_some(), "Should have entries key");
1866        assert!(store.get("lastId").is_some(), "Should have lastId key");
1867        assert!(store.get("facts").is_some(), "Should have facts key");
1868        assert!(store.get("lastFactId").is_some(), "Should have lastFactId key");
1869
1870        let entry = &store["entries"][0];
1871        assert!(entry.get("key").is_some(), "Entry should have key");
1872        assert!(entry.get("value").is_some(), "Entry should have value");
1873        assert!(entry.get("namespace").is_some(), "Entry should have namespace");
1874        assert!(entry.get("tags").is_some(), "Entry should have tags");
1875        assert!(entry.get("created").is_some(), "Entry should have created");
1876        assert!(entry.get("updated").is_some(), "Entry should have updated");
1877
1878        assert_eq!(entry["key"], "compat-key");
1879        assert_eq!(entry["value"], "Cross-runtime test");
1880        assert_eq!(entry["namespace"], "blue-red");
1881    }
1882
1883    #[tokio::test]
1884    async fn test_namespace_default_maps_to_project() {
1885        let (tool, _tmp) = test_tool();
1886
1887        // Create with namespace "default" (should map to Project scope)
1888        let args = MemoryToolArgs {
1889            action: "create".to_string(),
1890            statements: Some(vec!["Default namespace test".to_string()]),
1891            namespace: Some("default".to_string()),
1892            ..Default::default()
1893        };
1894        tool.execute(args).await.unwrap();
1895
1896        // Recall with scope "project" should find it
1897        let args = MemoryToolArgs {
1898            action: "recall".to_string(),
1899            queries: Some(vec!["Default namespace".to_string()]),
1900            scope: Some("project".to_string()),
1901            ..Default::default()
1902        };
1903        let result = tool.execute(args).await.unwrap();
1904        assert!(result.contains("Default namespace test"), "default namespace should map to project scope: {}", result);
1905    }
1906
1907    #[tokio::test]
1908    async fn test_persistence_with_facts() {
1909        let tmp = NamedTempFile::new().unwrap();
1910        let path = tmp.path().to_path_buf();
1911
1912        // Store facts with first instance
1913        {
1914            let tool = MemoryTool::with_path(Some(path.clone()));
1915            let args = MemoryToolArgs {
1916                action: "facts".to_string(),
1917                kb_name: Some("coding".to_string()),
1918                facts: Some(vec!["Always use uv for Python".to_string()]),
1919                ..Default::default()
1920            };
1921            tool.execute(args).await.unwrap();
1922        }
1923
1924        // Load with second instance and query
1925        {
1926            let tool = MemoryTool::with_path(Some(path.clone()));
1927            let args = MemoryToolArgs {
1928                action: "facts".to_string(),
1929                kb_name: Some("coding".to_string()),
1930                query: Some("uv".to_string()),
1931                ..Default::default()
1932            };
1933            let result = tool.execute(args).await.unwrap();
1934            assert!(result.contains("Always use uv"), "Facts should survive restart: {}", result);
1935        }
1936    }
1937}