use std::fmt;
use serde::{Deserialize, Serialize};
use uuid7::Uuid;
use crate::version::{schema::SchemaVersion, wire::WireVersion, VersionedEvent};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OpId(pub Uuid);
impl OpId {
pub fn new_random() -> Self {
Self(uuid7::uuid7())
}
pub fn from_uuid(u: Uuid) -> Self {
Self(u)
}
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
impl fmt::Display for OpId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TenantId(pub i64);
impl TenantId {
pub const fn new(id: i64) -> Self {
Self(id)
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum MemoryMutation {
UpsertMemory {
rid: String,
text: String,
memory_type: String,
importance: f64,
valence: f64,
half_life: f64,
namespace: String,
certainty: f64,
domain: String,
source: String,
emotional_state: Option<String>,
embedding: Option<Vec<f32>>,
metadata: serde_json::Value,
#[serde(default)]
extracted_entities: Vec<String>,
#[serde(default)]
created_at_unix_micros: Option<i64>,
#[serde(default)]
embedding_model: Option<String>,
},
UpdateMemoryPatch {
rid: String,
text: Option<String>,
importance: Option<f64>,
valence: Option<f64>,
certainty: Option<f64>,
metadata_patch: Option<serde_json::Value>,
},
TombstoneMemory {
rid: String,
reason: Option<String>,
requested_at_unix_micros: i64,
#[serde(default)]
namespace: String,
},
PurgeMemory { rid: String, purge_epoch: u64 },
UpsertEntityEdge {
edge_id: String,
src: String,
dst: String,
rel_type: String,
weight: f64,
namespace: String,
},
DeleteEntityEdge {
edge_id: String,
#[serde(default)]
namespace: String,
#[serde(default)]
requested_at_unix_micros: i64,
},
TenantConfigPatch {
key: String,
value: serde_json::Value,
},
}
impl MemoryMutation {
pub fn variant_name(&self) -> &'static str {
match self {
MemoryMutation::UpsertMemory { .. } => "UpsertMemory",
MemoryMutation::UpdateMemoryPatch { .. } => "UpdateMemoryPatch",
MemoryMutation::TombstoneMemory { .. } => "TombstoneMemory",
MemoryMutation::PurgeMemory { .. } => "PurgeMemory",
MemoryMutation::UpsertEntityEdge { .. } => "UpsertEntityEdge",
MemoryMutation::DeleteEntityEdge { .. } => "DeleteEntityEdge",
MemoryMutation::TenantConfigPatch { .. } => "TenantConfigPatch",
}
}
pub fn is_implemented(&self) -> bool {
match self {
MemoryMutation::UpsertMemory { .. }
| MemoryMutation::UpdateMemoryPatch { .. }
| MemoryMutation::UpsertEntityEdge { .. }
| MemoryMutation::DeleteEntityEdge { .. } => true,
MemoryMutation::TombstoneMemory { .. } => true,
MemoryMutation::PurgeMemory { .. } => false,
MemoryMutation::TenantConfigPatch { .. } => false,
}
}
pub fn planned_rfc(&self) -> &'static str {
match self {
MemoryMutation::TombstoneMemory { .. } => "011",
MemoryMutation::PurgeMemory { .. } => "011",
MemoryMutation::TenantConfigPatch { .. } => "021",
_ => "010",
}
}
pub fn tombstoned_rid(&self) -> Option<&str> {
match self {
MemoryMutation::TombstoneMemory { rid, .. } => Some(rid),
_ => None,
}
}
pub fn purged_rid(&self) -> Option<&str> {
match self {
MemoryMutation::PurgeMemory { rid, .. } => Some(rid),
_ => None,
}
}
pub fn feature_flag(&self) -> &'static str {
match self {
MemoryMutation::UpsertMemory { .. } => "mutation.UpsertMemory",
MemoryMutation::UpdateMemoryPatch { .. } => "mutation.UpdateMemoryPatch",
MemoryMutation::TombstoneMemory { .. } => "mutation.TombstoneMemory",
MemoryMutation::PurgeMemory { .. } => "mutation.PurgeMemory",
MemoryMutation::UpsertEntityEdge { .. } => "mutation.UpsertEntityEdge",
MemoryMutation::DeleteEntityEdge { .. } => "mutation.DeleteEntityEdge",
MemoryMutation::TenantConfigPatch { .. } => "mutation.TenantConfigPatch",
}
}
pub fn wire_introduced_at(&self) -> WireVersion {
WireVersion::new(1, 0)
}
}
impl VersionedEvent for MemoryMutation {
fn wire_version(&self) -> WireVersion {
self.wire_introduced_at()
}
fn schema_version(&self) -> Option<(&'static str, SchemaVersion)> {
match self {
MemoryMutation::UpsertMemory { .. }
| MemoryMutation::UpdateMemoryPatch { .. }
| MemoryMutation::TombstoneMemory { .. }
| MemoryMutation::PurgeMemory { .. } => {
Some(("memory_commit_log", SchemaVersion::new(1)))
}
MemoryMutation::UpsertEntityEdge { .. } | MemoryMutation::DeleteEntityEdge { .. } => {
Some(("memory_commit_log", SchemaVersion::new(1)))
}
MemoryMutation::TenantConfigPatch { .. } => {
Some(("tenant_config_overrides", SchemaVersion::new(1)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn upsert_example() -> MemoryMutation {
MemoryMutation::UpsertMemory {
rid: "mem_test_1".into(),
text: "the cat sat on the mat".into(),
memory_type: "semantic".into(),
importance: 0.5,
valence: 0.0,
half_life: 168.0,
namespace: "default".into(),
certainty: 1.0,
domain: "general".into(),
source: "user".into(),
emotional_state: None,
embedding: None,
extracted_entities: vec![],
created_at_unix_micros: None,
embedding_model: None,
metadata: serde_json::json!({}),
}
}
#[test]
fn op_id_is_unique_each_call() {
let a = OpId::new_random();
let b = OpId::new_random();
assert_ne!(a, b, "uuid7 collision is astronomically unlikely");
}
#[test]
fn op_id_serde_is_transparent_string() {
let id = OpId::new_random();
let json = serde_json::to_string(&id).unwrap();
assert!(json.starts_with('"') && json.ends_with('"'));
let back: OpId = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[test]
fn tenant_id_serde_is_transparent_integer() {
let t = TenantId::new(42);
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "42");
let back: TenantId = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn variant_names_are_stable_and_unique() {
let muts = vec![
upsert_example(),
MemoryMutation::UpdateMemoryPatch {
rid: "x".into(),
text: None,
importance: None,
valence: None,
certainty: None,
metadata_patch: None,
},
MemoryMutation::TombstoneMemory {
rid: "x".into(),
reason: None,
requested_at_unix_micros: 0,
namespace: String::new(),
},
MemoryMutation::PurgeMemory {
rid: "x".into(),
purge_epoch: 0,
},
MemoryMutation::UpsertEntityEdge {
edge_id: "e".into(),
src: "a".into(),
dst: "b".into(),
rel_type: "rel".into(),
weight: 1.0,
namespace: "default".into(),
},
MemoryMutation::DeleteEntityEdge {
edge_id: "e".into(),
namespace: String::new(),
requested_at_unix_micros: 0,
},
MemoryMutation::TenantConfigPatch {
key: "k".into(),
value: serde_json::Value::Null,
},
];
let names: Vec<_> = muts.iter().map(|m| m.variant_name()).collect();
let mut seen = std::collections::HashSet::new();
for name in &names {
assert!(seen.insert(*name), "duplicate variant_name: {name}");
}
assert_eq!(muts[0].variant_name(), "UpsertMemory");
assert_eq!(muts[2].variant_name(), "TombstoneMemory");
assert_eq!(muts[3].variant_name(), "PurgeMemory");
}
#[test]
fn implementation_status_matches_rfc_plan() {
assert!(upsert_example().is_implemented());
assert!(MemoryMutation::DeleteEntityEdge {
edge_id: "e".into(),
namespace: String::new(),
requested_at_unix_micros: 0
}
.is_implemented());
assert!(MemoryMutation::TombstoneMemory {
rid: "x".into(),
reason: None,
requested_at_unix_micros: 0,
namespace: String::new(),
}
.is_implemented());
assert!(!MemoryMutation::PurgeMemory {
rid: "x".into(),
purge_epoch: 0,
}
.is_implemented());
assert!(!MemoryMutation::TenantConfigPatch {
key: "k".into(),
value: serde_json::Value::Null,
}
.is_implemented());
}
#[test]
fn tombstoned_rid_extracts_only_for_tombstone_variant() {
let t = MemoryMutation::TombstoneMemory {
rid: "mem_a".into(),
reason: None,
requested_at_unix_micros: 0,
namespace: String::new(),
};
assert_eq!(t.tombstoned_rid(), Some("mem_a"));
assert_eq!(t.purged_rid(), None);
let p = MemoryMutation::PurgeMemory {
rid: "mem_b".into(),
purge_epoch: 1,
};
assert_eq!(p.purged_rid(), Some("mem_b"));
assert_eq!(p.tombstoned_rid(), None);
assert_eq!(upsert_example().tombstoned_rid(), None);
assert_eq!(upsert_example().purged_rid(), None);
}
#[test]
fn planned_rfc_points_to_correct_followup() {
assert_eq!(
MemoryMutation::TombstoneMemory {
rid: "x".into(),
reason: None,
requested_at_unix_micros: 0,
namespace: String::new(),
}
.planned_rfc(),
"011"
);
assert_eq!(
MemoryMutation::PurgeMemory {
rid: "x".into(),
purge_epoch: 0,
}
.planned_rfc(),
"011"
);
assert_eq!(
MemoryMutation::TenantConfigPatch {
key: "k".into(),
value: serde_json::Value::Null,
}
.planned_rfc(),
"021"
);
}
#[test]
fn versioned_event_returns_wire_1_0() {
let m = upsert_example();
let wv = m.wire_version();
assert_eq!(wv.major, 1);
assert_eq!(wv.minor, 0);
}
#[test]
fn versioned_event_schema_targets_correct_table() {
let m = upsert_example();
let (table, ver) = m.schema_version().unwrap();
assert_eq!(table, "memory_commit_log");
assert_eq!(ver, SchemaVersion::new(1));
let cfg = MemoryMutation::TenantConfigPatch {
key: "k".into(),
value: serde_json::Value::Null,
};
let (table, ver) = cfg.schema_version().unwrap();
assert_eq!(table, "tenant_config_overrides");
assert_eq!(ver, SchemaVersion::new(1));
}
#[test]
fn mutation_serde_round_trip_is_lossless() {
let cases = vec![
upsert_example(),
MemoryMutation::TombstoneMemory {
rid: "mem_42".into(),
reason: Some("user requested".into()),
requested_at_unix_micros: 1_700_000_000_000_000,
namespace: String::new(),
},
MemoryMutation::UpsertEntityEdge {
edge_id: "edge_7".into(),
src: "alice".into(),
dst: "bob".into(),
rel_type: "knows".into(),
weight: 0.9,
namespace: "default".into(),
},
];
for m in cases {
let json = serde_json::to_string(&m).unwrap();
let back: MemoryMutation = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
}
#[test]
fn tagged_serde_includes_kind_field() {
let m = MemoryMutation::TombstoneMemory {
rid: "x".into(),
reason: None,
requested_at_unix_micros: 0,
namespace: String::new(),
};
let json = serde_json::to_string(&m).unwrap();
assert!(
json.contains("\"kind\":\"TombstoneMemory\""),
"missing tag: {json}"
);
}
}