use anyhow::Result;
use oxidized_state::{CommitId, MemoryRecord, SurrealHandle};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VectorStoreDelta {
pub only_in_a: Vec<MemoryRecord>,
pub only_in_b: Vec<MemoryRecord>,
pub identical: Vec<MemoryRecord>,
pub conflicts: Vec<MemoryConflict>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConflict {
pub key: String,
pub memory_a: MemoryRecord,
pub memory_b: MemoryRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoResolvedValue {
pub value: String,
pub favored_branch: Option<String>,
pub reasoning: String,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeResult {
pub merge_commit_id: CommitId,
pub auto_resolved: usize,
pub manual_conflicts: Vec<MemoryConflict>,
pub summary: String,
}
pub async fn diff_memory_vectors(
handle: &SurrealHandle,
commit_a: &str,
commit_b: &str,
) -> Result<VectorStoreDelta> {
let memories_a = handle.get_memories(commit_a).await?;
let memories_b = handle.get_memories(commit_b).await?;
let keys_a: std::collections::HashSet<_> = memories_a.iter().map(|m| &m.key).collect();
let keys_b: std::collections::HashSet<_> = memories_b.iter().map(|m| &m.key).collect();
let only_in_a: Vec<_> = memories_a
.iter()
.filter(|m| !keys_b.contains(&m.key))
.cloned()
.collect();
let only_in_b: Vec<_> = memories_b
.iter()
.filter(|m| !keys_a.contains(&m.key))
.cloned()
.collect();
let mut conflicts = Vec::new();
let mut identical = Vec::new();
for mem_a in &memories_a {
if let Some(mem_b) = memories_b.iter().find(|m| m.key == mem_a.key) {
if mem_a.content != mem_b.content {
conflicts.push(MemoryConflict {
key: mem_a.key.clone(),
memory_a: mem_a.clone(),
memory_b: mem_b.clone(),
});
} else {
identical.push(mem_a.clone());
}
}
}
Ok(VectorStoreDelta {
only_in_a,
only_in_b,
identical,
conflicts,
})
}
pub async fn resolve_conflict_state(
_trace_a: &[serde_json::Value],
_trace_b: &[serde_json::Value],
conflict: &MemoryConflict,
) -> Result<AutoResolvedValue> {
let (value, favored, reasoning) =
if conflict.memory_a.content.len() >= conflict.memory_b.content.len() {
(
conflict.memory_a.content.clone(),
Some("A".to_string()),
"Chose branch A: more detailed content".to_string(),
)
} else {
(
conflict.memory_b.content.clone(),
Some("B".to_string()),
"Chose branch B: more detailed content".to_string(),
)
};
Ok(AutoResolvedValue {
value,
favored_branch: favored,
reasoning,
confidence: 0.6, })
}
pub async fn synthesize_memory(
handle: &SurrealHandle,
commit_a: &str,
commit_b: &str,
new_commit_id: &str,
) -> Result<Vec<MemoryRecord>> {
let delta = diff_memory_vectors(handle, commit_a, commit_b).await?;
let mut merged_memories = Vec::new();
for mut mem in delta.only_in_a {
mem.commit_id = new_commit_id.to_string();
mem.id = None;
merged_memories.push(mem);
}
for mut mem in delta.only_in_b {
mem.commit_id = new_commit_id.to_string();
mem.id = None;
merged_memories.push(mem);
}
for mut mem in delta.identical {
mem.commit_id = new_commit_id.to_string();
mem.id = None;
merged_memories.push(mem);
}
for conflict in delta.conflicts {
let resolved = resolve_conflict_state(&[], &[], &conflict).await?;
let merged_mem = MemoryRecord::new(new_commit_id, &conflict.key, &resolved.value)
.with_metadata(serde_json::json!({
"merged_from": [commit_a, commit_b],
"resolution": resolved.reasoning,
"confidence": resolved.confidence,
}));
merged_memories.push(merged_mem);
}
Ok(merged_memories)
}
pub async fn semantic_merge(
handle: &SurrealHandle,
commit_a: &str,
commit_b: &str,
message: &str,
author: &str,
) -> Result<MergeResult> {
let state_data = format!("merge:{}:{}", commit_a, commit_b);
let merge_commit_id = CommitId::from_state(state_data.as_bytes());
let merged_memories =
synthesize_memory(handle, commit_a, commit_b, &merge_commit_id.hash).await?;
for mem in &merged_memories {
handle.save_memory(mem).await?;
}
let commit = oxidized_state::CommitRecord::new(
merge_commit_id.clone(),
vec![commit_a.to_string(), commit_b.to_string()],
message,
author,
);
handle.save_commit(&commit).await?;
handle
.save_commit_graph_edge(&merge_commit_id.hash, commit_a)
.await?;
handle
.save_commit_graph_edge(&merge_commit_id.hash, commit_b)
.await?;
let delta = diff_memory_vectors(handle, commit_a, commit_b).await?;
Ok(MergeResult {
merge_commit_id,
auto_resolved: delta.conflicts.len(),
manual_conflicts: vec![], summary: format!(
"Merged {} memories from A, {} from B, resolved {} conflicts",
delta.only_in_a.len(),
delta.only_in_b.len(),
delta.conflicts.len()
),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_memory_diff_shows_only_new_vectors() {
let handle = SurrealHandle::setup_db().await.unwrap();
let mem_a1 = MemoryRecord::new("commit-a", "shared-key", "shared content");
let mem_a2 = MemoryRecord::new("commit-a", "only-a-key", "only in A");
handle.save_memory(&mem_a1).await.unwrap();
handle.save_memory(&mem_a2).await.unwrap();
let mem_b1 = MemoryRecord::new("commit-b", "shared-key", "shared content");
let mem_b2 = MemoryRecord::new("commit-b", "only-b-key", "only in B");
handle.save_memory(&mem_b1).await.unwrap();
handle.save_memory(&mem_b2).await.unwrap();
let delta = diff_memory_vectors(&handle, "commit-a", "commit-b")
.await
.unwrap();
assert_eq!(delta.only_in_a.len(), 1);
assert_eq!(delta.only_in_a[0].key, "only-a-key");
assert_eq!(delta.only_in_b.len(), 1);
assert_eq!(delta.only_in_b[0].key, "only-b-key");
assert_eq!(delta.conflicts.len(), 0); }
#[tokio::test]
async fn test_memory_diff_detects_conflicts() {
let handle = SurrealHandle::setup_db().await.unwrap();
let mem_a = MemoryRecord::new("commit-a", "conflict-key", "content version A");
let mem_b = MemoryRecord::new("commit-b", "conflict-key", "content version B");
handle.save_memory(&mem_a).await.unwrap();
handle.save_memory(&mem_b).await.unwrap();
let delta = diff_memory_vectors(&handle, "commit-a", "commit-b")
.await
.unwrap();
assert_eq!(delta.conflicts.len(), 1);
assert_eq!(delta.conflicts[0].key, "conflict-key");
}
#[tokio::test]
async fn test_arbiter_resolves_value_conflict_based_on_cot() {
let conflict = MemoryConflict {
key: "test-key".to_string(),
memory_a: MemoryRecord::new("a", "test-key", "short"),
memory_b: MemoryRecord::new("b", "test-key", "longer content here"),
};
let resolved = resolve_conflict_state(&[], &[], &conflict).await.unwrap();
assert!(resolved.confidence > 0.0);
assert!(resolved.favored_branch.is_some());
assert!(!resolved.reasoning.is_empty());
}
#[tokio::test]
async fn test_merge_synthesizes_two_memories_into_one_new_commit() {
let handle = SurrealHandle::setup_db().await.unwrap();
let commit_id_a = oxidized_state::CommitId::from_state(b"branch-a");
let commit_id_b = oxidized_state::CommitId::from_state(b"branch-b");
let commit_a = oxidized_state::CommitRecord::new(
commit_id_a.clone(),
vec![],
"Branch A commit",
"agent-a",
);
handle.save_commit(&commit_a).await.unwrap();
let commit_b = oxidized_state::CommitRecord::new(
commit_id_b.clone(),
vec![],
"Branch B commit",
"agent-b",
);
handle.save_commit(&commit_b).await.unwrap();
let mem_a_only =
MemoryRecord::new(&commit_id_a.hash, "learned-from-a", "Strategy A knowledge");
let mem_b_only =
MemoryRecord::new(&commit_id_b.hash, "learned-from-b", "Strategy B knowledge");
let mem_conflict_a = MemoryRecord::new(&commit_id_a.hash, "shared-key", "short");
let mem_conflict_b = MemoryRecord::new(
&commit_id_b.hash,
"shared-key",
"longer and more detailed content",
);
handle.save_memory(&mem_a_only).await.unwrap();
handle.save_memory(&mem_b_only).await.unwrap();
handle.save_memory(&mem_conflict_a).await.unwrap();
handle.save_memory(&mem_conflict_b).await.unwrap();
let result = semantic_merge(
&handle,
&commit_id_a.hash,
&commit_id_b.hash,
"Merge A and B",
"agent-git",
)
.await
.unwrap();
assert!(!result.merge_commit_id.hash.is_empty());
let merged_memories = handle
.get_memories(&result.merge_commit_id.hash)
.await
.unwrap();
assert_eq!(merged_memories.len(), 3, "Expected 3 merged memories");
let keys: Vec<_> = merged_memories.iter().map(|m| m.key.as_str()).collect();
assert!(
keys.contains(&"learned-from-a"),
"Missing memory from branch A"
);
assert!(
keys.contains(&"learned-from-b"),
"Missing memory from branch B"
);
assert!(keys.contains(&"shared-key"), "Missing resolved conflict");
let resolved = merged_memories
.iter()
.find(|m| m.key == "shared-key")
.unwrap();
assert!(
resolved.content.contains("longer") || resolved.content.contains("detailed"),
"Conflict resolution should favor more detailed content"
);
assert!(
result.summary.contains("2") || result.summary.contains("memories"),
"Summary should mention merged memories"
);
}
}