#[cfg(feature = "server-stack")]
use anyhow::{anyhow, Context, Result};
#[cfg(feature = "server-stack")]
use qdrant_client::qdrant::Value;
#[cfg(feature = "server-stack")]
use qdrant_client::Payload;
use serde::{Deserialize, Serialize};
#[cfg(feature = "server-stack")]
use std::collections::HashMap;
use uuid::Uuid;
const MEMORY_NS: Uuid = Uuid::from_bytes([
0x6c, 0x6f, 0x2d, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x2d, 0x76, 0x35, 0x6e, 0x73, 0x00, 0x01,
]);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryAnchor {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticMemory {
pub project_id: String,
pub bucket: String,
pub title: String,
pub content: String,
#[serde(default)]
pub anchors: Vec<MemoryAnchor>,
pub created_at: String,
pub updated_at: String,
}
impl SemanticMemory {
pub fn point_id(&self) -> Uuid {
point_id_for(&self.project_id, &self.bucket, &self.title)
}
}
pub fn point_id_for(project_id: &str, bucket: &str, title: &str) -> Uuid {
let key = format!("{project_id}\x1f{bucket}\x1f{title}");
Uuid::new_v5(&MEMORY_NS, key.as_bytes())
}
#[cfg(feature = "server-stack")]
pub fn memory_to_payload(m: &SemanticMemory) -> HashMap<String, Value> {
let mut map = HashMap::new();
map.insert("project_id".into(), Value::from(m.project_id.clone()));
map.insert("bucket".into(), Value::from(m.bucket.clone()));
map.insert("title".into(), Value::from(m.title.clone()));
map.insert("content".into(), Value::from(m.content.clone()));
map.insert("created_at".into(), Value::from(m.created_at.clone()));
map.insert("updated_at".into(), Value::from(m.updated_at.clone()));
let anchors: Vec<Value> = m
.anchors
.iter()
.map(|a| {
let mut inner: HashMap<String, Value> = HashMap::new();
inner.insert("path".into(), Value::from(a.path.clone()));
Value::from(Payload::from(inner))
})
.collect();
map.insert("anchors".into(), Value::from(anchors));
map
}
#[cfg(feature = "server-stack")]
pub fn payload_to_memory(m: &HashMap<String, Value>) -> Result<SemanticMemory> {
Ok(SemanticMemory {
project_id: get_str(m, "project_id")?,
bucket: get_str(m, "bucket")?,
title: get_str(m, "title")?,
content: get_str(m, "content")?,
anchors: get_anchors(m)?,
created_at: get_str(m, "created_at")?,
updated_at: get_str(m, "updated_at")?,
})
}
#[cfg(feature = "server-stack")]
fn get_str(m: &HashMap<String, Value>, key: &str) -> Result<String> {
m.get(key)
.ok_or_else(|| anyhow!("missing field: {key}"))?
.as_str()
.map(|s| s.as_str().to_owned())
.ok_or_else(|| anyhow!("field {key} is not a string"))
}
#[cfg(feature = "server-stack")]
fn get_anchors(m: &HashMap<String, Value>) -> Result<Vec<MemoryAnchor>> {
let Some(v) = m.get("anchors") else {
return Ok(vec![]);
};
let list = v
.as_list()
.ok_or_else(|| anyhow!("field anchors is not a list"))?;
list.iter()
.map(|item| {
let inner = item
.as_struct()
.ok_or_else(|| anyhow!("anchor item is not a struct"))?
.fields
.clone();
Ok(MemoryAnchor {
path: get_str(&inner, "path").context("anchor.path")?,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> SemanticMemory {
SemanticMemory {
project_id: "codescout".into(),
bucket: "system".into(),
title: "BUG-021 parallel writes".into(),
content: "Never dispatch parallel write tool calls...".into(),
anchors: vec![MemoryAnchor {
path: "src/tools/memory/mod.rs".into(),
}],
created_at: "2026-03-03T12:00:00Z".into(),
updated_at: "2026-05-13T05:00:00Z".into(),
}
}
#[test]
fn point_id_is_stable_for_same_inputs() {
let m1 = sample();
let m2 = sample();
assert_eq!(m1.point_id(), m2.point_id());
}
#[test]
fn point_id_differs_across_projects() {
let m1 = sample();
let mut m2 = sample();
m2.project_id = "other-project".into();
assert_ne!(m1.point_id(), m2.point_id());
}
#[test]
fn point_id_differs_across_buckets() {
let m1 = sample();
let mut m2 = sample();
m2.bucket = "preferences".into();
assert_ne!(m1.point_id(), m2.point_id());
}
#[cfg(feature = "server-stack")]
#[test]
fn payload_roundtrip() {
let m = sample();
let payload = memory_to_payload(&m);
let back = payload_to_memory(&payload).expect("roundtrip");
assert_eq!(back.project_id, m.project_id);
assert_eq!(back.bucket, m.bucket);
assert_eq!(back.title, m.title);
assert_eq!(back.content, m.content);
assert_eq!(back.anchors, m.anchors);
assert_eq!(back.created_at, m.created_at);
assert_eq!(back.updated_at, m.updated_at);
}
#[cfg(feature = "server-stack")]
#[test]
fn payload_roundtrip_no_anchors() {
let mut m = sample();
m.anchors.clear();
let payload = memory_to_payload(&m);
let back = payload_to_memory(&payload).expect("roundtrip");
assert!(back.anchors.is_empty());
}
}