Skip to main content

oxios_kernel/memory/
auto_memory_bridge.rs

1//! Auto-memory bridge: synchronization between external memory systems and Oxios.
2//!
3//! Bridges Oxios MemoryManager with Claude Code's MEMORY.md format and
4//! similar external memory stores. Supports bidirectional sync:
5//!
6//! - **to-auto**: Export Oxios patterns/insights → external MEMORY.md format
7//! - **from-auto**: Import external memories → Oxios MemoryStore
8//! - **bidirectional**: Full two-way synchronization
9//!
10//! The bridge converts between Oxios's structured memory entries and
11//! free-form markdown memory formats used by external tools.
12
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use anyhow::Result;
17use chrono::Utc;
18use serde::{Deserialize, Serialize};
19
20use super::{MemoryEntry, MemoryManager, MemoryType};
21
22// ---------------------------------------------------------------------------
23// Types
24// ---------------------------------------------------------------------------
25
26/// Direction of memory synchronization.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum SyncDirection {
30    /// Export Oxios → external format.
31    ToAuto,
32    /// Import external → Oxios.
33    FromAuto,
34    /// Two-way sync.
35    Bidirectional,
36}
37
38/// Category of an imported memory insight.
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum InsightCategory {
42    /// Project-level patterns and conventions.
43    ProjectPatterns,
44    /// Debugging strategies and fixes.
45    Debugging,
46    /// Architecture decisions.
47    Architecture,
48    /// Performance observations.
49    Performance,
50    /// Security-related insights.
51    Security,
52    /// General knowledge.
53    General,
54}
55
56impl InsightCategory {
57    /// Parse from a string.
58    pub fn from_str_loose(s: &str) -> Self {
59        match s.to_lowercase().as_str() {
60            "project-patterns" | "project_patterns" | "patterns" => {
61                InsightCategory::ProjectPatterns
62            }
63            "debugging" | "debug" => InsightCategory::Debugging,
64            "architecture" | "arch" => InsightCategory::Architecture,
65            "performance" | "perf" => InsightCategory::Performance,
66            "security" | "sec" => InsightCategory::Security,
67            _ => InsightCategory::General,
68        }
69    }
70
71    /// Convert to a tag string.
72    pub fn to_tag(&self) -> &'static str {
73        match self {
74            InsightCategory::ProjectPatterns => "project-patterns",
75            InsightCategory::Debugging => "debugging",
76            InsightCategory::Architecture => "architecture",
77            InsightCategory::Performance => "performance",
78            InsightCategory::Security => "security",
79            InsightCategory::General => "general",
80        }
81    }
82}
83
84/// A single imported memory insight from an external system.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MemoryInsight {
87    /// Category of the insight.
88    pub category: InsightCategory,
89    /// Brief summary (1 line).
90    pub summary: String,
91    /// Optional detailed content.
92    pub detail: Option<String>,
93    /// Source identifier (e.g., "claude-code", "user").
94    pub source: String,
95    /// Confidence score (0.0 - 1.0).
96    pub confidence: f32,
97}
98
99/// Result of importing memories from an external system.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct ImportResult {
102    /// Number of insights successfully imported.
103    pub imported: usize,
104    /// Number of duplicates skipped.
105    pub skipped_duplicates: usize,
106    /// Number of failed imports.
107    pub failed: usize,
108    /// Error messages for failed imports.
109    pub errors: Vec<String>,
110}
111
112/// Result of exporting memories to an external system.
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct ExportResult {
115    /// Number of patterns exported.
116    pub exported: usize,
117    /// Number of categories created/updated.
118    pub categories_updated: usize,
119}
120
121/// Result of a bidirectional sync operation.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct SyncResult {
124    /// Import results.
125    pub import: ImportResult,
126    /// Export results.
127    pub export: ExportResult,
128}
129
130/// A guidance pattern for export to external format.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct GuidancePattern {
133    /// Pattern identifier.
134    pub id: String,
135    /// Category.
136    pub category: InsightCategory,
137    /// Brief description.
138    pub description: String,
139    /// Confidence/importance.
140    pub confidence: f32,
141    /// Usage count.
142    pub usage_count: u32,
143}
144
145// ---------------------------------------------------------------------------
146// AutoMemoryBridge
147// ---------------------------------------------------------------------------
148
149/// Bridge between Oxios memory and external memory systems.
150///
151/// Reads/writes MEMORY.md-style files and synchronizes with Oxios's
152/// structured MemoryManager.
153pub struct AutoMemoryBridge {
154    /// Directory containing external memory files.
155    auto_memory_dir: PathBuf,
156    /// Oxios memory manager.
157    oxios_memory: std::sync::Arc<MemoryManager>,
158}
159
160impl AutoMemoryBridge {
161    /// Create a new bridge.
162    ///
163    /// # Arguments
164    /// * `auto_memory_dir` - Path to directory containing MEMORY.md files
165    /// * `oxios_memory` - Oxios MemoryManager instance
166    pub fn new(auto_memory_dir: PathBuf, oxios_memory: std::sync::Arc<MemoryManager>) -> Self {
167        Self {
168            auto_memory_dir,
169            oxios_memory,
170        }
171    }
172
173    /// Import memories from external format into Oxios.
174    ///
175    /// Reads MEMORY.md files from the auto_memory_dir, parses them into
176    /// structured insights, and stores them via MemoryManager.
177    pub async fn import_from_auto(&self) -> Result<ImportResult> {
178        let mut result = ImportResult::default();
179
180        // Find all MEMORY.md files
181        let memory_files = self.find_memory_files()?;
182
183        for file_path in &memory_files {
184            match self.import_file(file_path).await {
185                Ok(file_result) => {
186                    result.imported += file_result.imported;
187                    result.skipped_duplicates += file_result.skipped_duplicates;
188                    result.failed += file_result.failed;
189                    result.errors.extend(file_result.errors);
190                }
191                Err(e) => {
192                    result.failed += 1;
193                    result
194                        .errors
195                        .push(format!("{}: {}", file_path.display(), e));
196                }
197            }
198        }
199
200        Ok(result)
201    }
202
203    /// Export Oxios patterns to external MEMORY.md format.
204    ///
205    /// Reads patterns from Oxios memory and writes them as structured
206    /// markdown files in the auto_memory_dir.
207    pub async fn export_to_auto(&self, patterns: &[GuidancePattern]) -> Result<ExportResult> {
208        let mut result = ExportResult::default();
209
210        // Ensure output directory exists
211        tokio::fs::create_dir_all(&self.auto_memory_dir).await?;
212
213        // Group patterns by category
214        let mut by_category: HashMap<InsightCategory, Vec<&GuidancePattern>> = HashMap::new();
215        for pattern in patterns {
216            by_category
217                .entry(pattern.category.clone())
218                .or_default()
219                .push(pattern);
220        }
221
222        // Write each category to its own file
223        for (category, cat_patterns) in &by_category {
224            let filename = match category {
225                InsightCategory::ProjectPatterns => "patterns.md",
226                InsightCategory::Debugging => "debugging.md",
227                InsightCategory::Architecture => "architecture.md",
228                InsightCategory::Performance => "performance.md",
229                InsightCategory::Security => "security.md",
230                InsightCategory::General => "general.md",
231            };
232
233            let content = self.format_patterns_md(cat_patterns);
234            let path = self.auto_memory_dir.join(filename);
235
236            tokio::fs::write(&path, &content).await?;
237            result.categories_updated += 1;
238        }
239
240        // Write main MEMORY.md with all patterns sorted by confidence
241        let mut all_patterns: Vec<&GuidancePattern> = patterns.iter().collect();
242        all_patterns.sort_by(|a, b| {
243            b.confidence
244                .partial_cmp(&a.confidence)
245                .unwrap_or(std::cmp::Ordering::Equal)
246        });
247
248        let main_content = self.format_main_md(&all_patterns);
249        let main_path = self.auto_memory_dir.join("MEMORY.md");
250        tokio::fs::write(&main_path, &main_content).await?;
251
252        result.exported = patterns.len();
253        result.categories_updated += 1; // MEMORY.md itself
254        Ok(result)
255    }
256
257    /// Perform a bidirectional sync.
258    pub async fn sync_session(&self, direction: SyncDirection) -> Result<SyncResult> {
259        let mut sync_result = SyncResult::default();
260
261        match direction {
262            SyncDirection::FromAuto => {
263                sync_result.import = self.import_from_auto().await?;
264            }
265            SyncDirection::ToAuto => {
266                sync_result.export = self.export_knowledge_to_auto().await?;
267            }
268            SyncDirection::Bidirectional => {
269                sync_result.import = self.import_from_auto().await?;
270                sync_result.export = self.export_knowledge_to_auto().await?;
271            }
272        }
273
274        Ok(sync_result)
275    }
276
277    /// Returns the auto-memory directory path.
278    pub fn auto_memory_dir(&self) -> &Path {
279        &self.auto_memory_dir
280    }
281
282    /// Export all knowledge memories from Oxios to external format.
283    async fn export_knowledge_to_auto(&self) -> Result<ExportResult> {
284        let entries = self
285            .oxios_memory
286            .list(MemoryType::Knowledge, 1000)
287            .await
288            .unwrap_or_default();
289
290        let patterns: Vec<GuidancePattern> = entries
291            .iter()
292            .map(|e| GuidancePattern {
293                id: e.id.clone(),
294                category: e
295                    .tags
296                    .first()
297                    .map(|t| InsightCategory::from_str_loose(t))
298                    .unwrap_or(InsightCategory::General),
299                description: e.content.clone(),
300                confidence: e.importance,
301                usage_count: e.access_count,
302            })
303            .collect();
304
305        self.export_to_auto(&patterns).await
306    }
307}
308
309// ---------------------------------------------------------------------------
310// Private helpers
311// ---------------------------------------------------------------------------
312
313impl AutoMemoryBridge {
314    /// Find all MEMORY.md files in the auto_memory_dir.
315    fn find_memory_files(&self) -> Result<Vec<PathBuf>> {
316        let mut files = Vec::new();
317
318        if self.auto_memory_dir.exists() {
319            // Look for MEMORY.md directly
320            let main = self.auto_memory_dir.join("MEMORY.md");
321            if main.exists() {
322                files.push(main);
323            }
324
325            // Look for topic files
326            for topic in &[
327                "patterns.md",
328                "debugging.md",
329                "architecture.md",
330                "performance.md",
331                "security.md",
332                "general.md",
333            ] {
334                let path = self.auto_memory_dir.join(topic);
335                if path.exists() {
336                    files.push(path);
337                }
338            }
339
340            // Look for *.md files in subdirectories
341            if let Ok(entries) = std::fs::read_dir(&self.auto_memory_dir) {
342                for entry in entries.flatten() {
343                    let path = entry.path();
344                    if path.extension().is_some_and(|ext| ext == "md") {
345                        let name = path.file_name().unwrap_or_default().to_string_lossy();
346                        // Skip already-added files
347                        if ![
348                            "MEMORY.md",
349                            "patterns.md",
350                            "debugging.md",
351                            "architecture.md",
352                            "performance.md",
353                            "security.md",
354                            "general.md",
355                        ]
356                        .contains(&name.as_ref())
357                        {
358                            files.push(path);
359                        }
360                    }
361                }
362            }
363        }
364
365        Ok(files)
366    }
367
368    /// Import a single markdown file into Oxios memory.
369    async fn import_file(&self, path: &Path) -> Result<ImportResult> {
370        let content = tokio::fs::read_to_string(path).await?;
371        let insights = self.parse_markdown_insights(&content);
372        let mut result = ImportResult::default();
373
374        for insight in &insights {
375            // Check for duplicates
376            if self.oxios_memory.is_duplicate(&insight.summary).await {
377                result.skipped_duplicates += 1;
378                continue;
379            }
380
381            let entry = MemoryEntry {
382                id: format!(
383                    "auto-{}-{}",
384                    insight.source,
385                    chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
386                ),
387                memory_type: MemoryType::Knowledge,
388                content: match &insight.detail {
389                    Some(d) => format!("{}\n\n{}", insight.summary, d),
390                    None => insight.summary.clone(),
391                },
392                source: insight.source.clone(),
393                session_id: None,
394                tags: vec![insight.category.to_tag().to_string()],
395                importance: insight.confidence,
396                created_at: Utc::now(),
397                accessed_at: Utc::now(),
398                access_count: 0,
399            };
400
401            match self.oxios_memory.remember(entry).await {
402                Ok(_) => result.imported += 1,
403                Err(e) => {
404                    result.failed += 1;
405                    result.errors.push(e.to_string());
406                    tracing::warn!(error = %e, "Failed to import memory insight");
407                }
408            }
409        }
410
411        Ok(result)
412    }
413
414    /// Parse insights from markdown content.
415    ///
416    /// Supports multiple formats:
417    /// - List items: `- **Category**: Description`
418    /// - Headers: `## Category` followed by bullet points
419    /// - Free-form text split into chunks
420    fn parse_markdown_insights(&self, content: &str) -> Vec<MemoryInsight> {
421        let mut insights = Vec::new();
422        let mut current_category = InsightCategory::General;
423
424        for line in content.lines() {
425            let trimmed = line.trim();
426
427            // Skip empty lines
428            if trimmed.is_empty() {
429                continue;
430            }
431
432            // Detect category headers: ## Category or # Category
433            if trimmed.starts_with('#') {
434                let header = trimmed.trim_start_matches('#').trim().to_lowercase();
435                current_category = InsightCategory::from_str_loose(&header);
436                continue;
437            }
438
439            // Parse bullet list items: - **Category**: Description
440            if trimmed.starts_with('-') || trimmed.starts_with('*') {
441                // Strip the leading list marker and whitespace, preserving ** bold markers
442                let item = if trimmed.starts_with('-') {
443                    trimmed.trim_start_matches('-').trim_start()
444                } else {
445                    trimmed.trim_start_matches('*').trim_start()
446                };
447
448                // Check for bold category prefix: **Category**:
449                if let Some(rest) = Self::extract_bold_category(item) {
450                    let (cat_name, description) = rest;
451                    let category = InsightCategory::from_str_loose(cat_name);
452
453                    // Split on colon for detail
454                    let (summary, detail) = if let Some(pos) = description.find(':') {
455                        let s = description[..pos].trim();
456                        let d = description[pos + 1..].trim();
457                        (
458                            s.to_string(),
459                            if d.is_empty() {
460                                None
461                            } else {
462                                Some(d.to_string())
463                            },
464                        )
465                    } else {
466                        (description.to_string(), None)
467                    };
468
469                    if !summary.is_empty() {
470                        insights.push(MemoryInsight {
471                            category,
472                            summary,
473                            detail,
474                            source: "auto-import".to_string(),
475                            confidence: 0.7,
476                        });
477                    }
478                } else if !item.is_empty() {
479                    // Plain bullet point
480                    insights.push(MemoryInsight {
481                        category: current_category.clone(),
482                        summary: item.to_string(),
483                        detail: None,
484                        source: "auto-import".to_string(),
485                        confidence: 0.6,
486                    });
487                }
488            }
489        }
490
491        // If no structured content found, treat the whole content as a single insight
492        if insights.is_empty() && !content.trim().is_empty() {
493            let summary: String = content
494                .lines()
495                .take(3)
496                .collect::<Vec<_>>()
497                .join(" ")
498                .chars()
499                .take(200)
500                .collect();
501
502            if !summary.trim().is_empty() {
503                insights.push(MemoryInsight {
504                    category: InsightCategory::General,
505                    summary,
506                    detail: Some(content.to_string()),
507                    source: "auto-import".to_string(),
508                    confidence: 0.5,
509                });
510            }
511        }
512
513        insights
514    }
515
516    /// Extract a bold category prefix from a markdown item.
517    ///
518    /// Returns Some((category, rest)) if the item starts with **Category**:
519    fn extract_bold_category(item: &str) -> Option<(&str, &str)> {
520        if !item.starts_with("**") {
521            return None;
522        }
523
524        let end = item[2..].find("**")?;
525        let category = &item[2..2 + end];
526        let rest = item[2 + end + 2..].trim_start_matches([' ', ':']);
527
528        Some((category, rest))
529    }
530
531    /// Format patterns as a category-specific markdown file.
532    fn format_patterns_md(&self, patterns: &[&GuidancePattern]) -> String {
533        let mut md = String::new();
534        md.push_str("# Memory Insights\n\n");
535
536        for pattern in patterns {
537            let confidence_bar = format_confidence_bar(pattern.confidence);
538            md.push_str(&format!(
539                "- **{}**: {} [{}]\n",
540                pattern.category.to_tag(),
541                pattern.description,
542                confidence_bar,
543            ));
544        }
545
546        md.push('\n');
547        md
548    }
549
550    /// Format all patterns as the main MEMORY.md file.
551    fn format_main_md(&self, patterns: &[&GuidancePattern]) -> String {
552        let mut md = String::new();
553        md.push_str("# Oxios Memory\n\n");
554        md.push_str(&format!(
555            "Auto-generated at {}\n\n",
556            Utc::now().to_rfc3339()
557        ));
558        md.push_str("## Insights\n\n");
559
560        for pattern in patterns {
561            let confidence_pct = (pattern.confidence * 100.0) as u8;
562            md.push_str(&format!(
563                "- **{}** [{}%]: {}\n",
564                pattern.category.to_tag(),
565                confidence_pct,
566                pattern.description,
567            ));
568        }
569
570        md.push('\n');
571        md
572    }
573}
574
575/// Format a confidence value as a visual bar.
576fn format_confidence_bar(confidence: f32) -> String {
577    let bars = (confidence * 5.0).round() as usize;
578    let bars = bars.min(5);
579    let filled: String = "█".repeat(bars);
580    let empty: String = "░".repeat(5 - bars);
581    format!("{}{} {:.0}%", filled, empty, confidence * 100.0)
582}
583
584// ---------------------------------------------------------------------------
585// Tests
586// ---------------------------------------------------------------------------
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use std::sync::Arc;
592
593    fn make_bridge(dir: &Path) -> AutoMemoryBridge {
594        let store = Arc::new(crate::state_store::StateStore::new(dir.join("state")).unwrap());
595        let memory = Arc::new(MemoryManager::new(store));
596        AutoMemoryBridge::new(dir.join("auto"), memory)
597    }
598
599    #[test]
600    fn test_parse_bold_category() {
601        let result =
602            AutoMemoryBridge::extract_bold_category("**Debugging**: Use trace-level logging");
603        assert!(result.is_some());
604        let (cat, rest) = result.unwrap();
605        assert_eq!(cat, "Debugging");
606        assert_eq!(rest, "Use trace-level logging");
607    }
608
609    #[test]
610    fn test_parse_bold_category_no_colon() {
611        let result = AutoMemoryBridge::extract_bold_category("**Security** important rule");
612        assert!(result.is_some());
613        let (cat, rest) = result.unwrap();
614        assert_eq!(cat, "Security");
615        assert_eq!(rest, "important rule");
616    }
617
618    #[test]
619    fn test_parse_no_bold() {
620        let result = AutoMemoryBridge::extract_bold_category("Just a plain item");
621        assert!(result.is_none());
622    }
623
624    #[test]
625    fn test_parse_markdown_insights() {
626        let temp_dir = tempfile::tempdir().unwrap();
627        let bridge = make_bridge(temp_dir.path());
628
629        let md = r#"# Project Patterns
630
631- **Debugging**: Use trace-level logging for async tasks
632- **Architecture**: Follow the kernel module pattern
633- **Performance**: Batch embeddings when possible
634
635## Security
636
637- Always validate input at the boundary
638- Use RBAC for multi-agent access control
639"#;
640
641        let insights = bridge.parse_markdown_insights(md);
642        assert!(!insights.is_empty());
643
644        // Should have parsed the bullet points
645        let debugging = insights
646            .iter()
647            .find(|i| i.category == InsightCategory::Debugging);
648        assert!(debugging.is_some());
649
650        let security = insights
651            .iter()
652            .find(|i| i.category == InsightCategory::Security);
653        assert!(security.is_some());
654    }
655
656    #[test]
657    fn test_parse_empty_markdown() {
658        let temp_dir = tempfile::tempdir().unwrap();
659        let bridge = make_bridge(temp_dir.path());
660
661        let insights = bridge.parse_markdown_insights("");
662        assert!(insights.is_empty());
663    }
664
665    #[test]
666    fn test_parse_plain_text_as_single_insight() {
667        let temp_dir = tempfile::tempdir().unwrap();
668        let bridge = make_bridge(temp_dir.path());
669
670        let text = "This is a plain text memory entry without markdown formatting.";
671        let insights = bridge.parse_markdown_insights(text);
672        assert_eq!(insights.len(), 1);
673        assert_eq!(insights[0].category, InsightCategory::General);
674    }
675
676    #[test]
677    fn test_insight_category_parsing() {
678        assert_eq!(
679            InsightCategory::from_str_loose("patterns"),
680            InsightCategory::ProjectPatterns
681        );
682        assert_eq!(
683            InsightCategory::from_str_loose("debug"),
684            InsightCategory::Debugging
685        );
686        assert_eq!(
687            InsightCategory::from_str_loose("arch"),
688            InsightCategory::Architecture
689        );
690        assert_eq!(
691            InsightCategory::from_str_loose("perf"),
692            InsightCategory::Performance
693        );
694        assert_eq!(
695            InsightCategory::from_str_loose("sec"),
696            InsightCategory::Security
697        );
698        assert_eq!(
699            InsightCategory::from_str_loose("unknown"),
700            InsightCategory::General
701        );
702    }
703
704    #[test]
705    fn test_confidence_bar() {
706        let bar = format_confidence_bar(0.8);
707        assert!(bar.contains("80%"));
708
709        let bar_low = format_confidence_bar(0.2);
710        assert!(bar_low.contains("20%"));
711    }
712
713    #[tokio::test]
714    async fn test_import_from_empty_dir() {
715        let temp_dir = tempfile::tempdir().unwrap();
716        let bridge = make_bridge(temp_dir.path());
717
718        // No files to import
719        let result = bridge.import_from_auto().await.unwrap();
720        assert_eq!(result.imported, 0);
721        assert_eq!(result.failed, 0);
722    }
723
724    #[tokio::test]
725    async fn test_import_from_file() {
726        let temp_dir = tempfile::tempdir().unwrap();
727        let auto_dir = temp_dir.path().join("auto");
728        tokio::fs::create_dir_all(&auto_dir).await.unwrap();
729
730        // Create a MEMORY.md file
731        let md = r#"- **Debugging**: Use println! for quick debugging
732- **Architecture**: Keep modules small and focused
733"#;
734        tokio::fs::write(auto_dir.join("MEMORY.md"), md)
735            .await
736            .unwrap();
737
738        let bridge = make_bridge(temp_dir.path());
739        let result = bridge.import_from_auto().await.unwrap();
740        assert!(result.imported >= 1, "Should import at least 1 insight");
741    }
742
743    #[tokio::test]
744    async fn test_export_to_auto() {
745        let temp_dir = tempfile::tempdir().unwrap();
746        let bridge = make_bridge(temp_dir.path());
747
748        let patterns = vec![
749            GuidancePattern {
750                id: "p1".to_string(),
751                category: InsightCategory::Debugging,
752                description: "Always check error chains".to_string(),
753                confidence: 0.9,
754                usage_count: 5,
755            },
756            GuidancePattern {
757                id: "p2".to_string(),
758                category: InsightCategory::Architecture,
759                description: "Use actor model for concurrency".to_string(),
760                confidence: 0.7,
761                usage_count: 3,
762            },
763        ];
764
765        let result = bridge.export_to_auto(&patterns).await.unwrap();
766        assert_eq!(result.exported, 2);
767        assert!(result.categories_updated >= 2);
768
769        // Verify files were created
770        assert!(bridge.auto_memory_dir.join("MEMORY.md").exists());
771        assert!(bridge.auto_memory_dir.join("debugging.md").exists());
772        assert!(bridge.auto_memory_dir.join("architecture.md").exists());
773
774        // Verify content
775        let main = tokio::fs::read_to_string(bridge.auto_memory_dir.join("MEMORY.md"))
776            .await
777            .unwrap();
778        assert!(main.contains("Oxios Memory"));
779        assert!(main.contains("Always check error chains"));
780        assert!(main.contains("Use actor model for concurrency"));
781    }
782
783    #[tokio::test]
784    async fn test_bidirectional_sync() {
785        let temp_dir = tempfile::tempdir().unwrap();
786        let auto_dir = temp_dir.path().join("auto");
787        tokio::fs::create_dir_all(&auto_dir).await.unwrap();
788
789        // Create an external memory file
790        tokio::fs::write(
791            auto_dir.join("MEMORY.md"),
792            "- **Debugging**: Test insight for sync",
793        )
794        .await
795        .unwrap();
796
797        let bridge = make_bridge(temp_dir.path());
798        let result = bridge
799            .sync_session(SyncDirection::Bidirectional)
800            .await
801            .unwrap();
802
803        // Should have imported the insight
804        assert!(result.import.imported >= 1 || result.import.skipped_duplicates > 0);
805
806        // Should have exported current state
807        assert!(result.export.exported >= 0);
808    }
809
810    #[test]
811    fn test_sync_direction_serialization() {
812        let dir = SyncDirection::ToAuto;
813        let json = serde_json::to_string(&dir).unwrap();
814        assert_eq!(json, "\"to_auto\"");
815
816        let parsed: SyncDirection = serde_json::from_str(&json).unwrap();
817        assert_eq!(parsed, SyncDirection::ToAuto);
818    }
819}