use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use ulid::Ulid;
pub fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum MemoryType {
Episodic,
Semantic,
Procedural,
Working,
}
impl MemoryType {
pub fn parse(s: &str) -> Option<MemoryType> {
match s.to_ascii_lowercase().as_str() {
"episodic" => Some(MemoryType::Episodic),
"semantic" => Some(MemoryType::Semantic),
"procedural" => Some(MemoryType::Procedural),
"working" => Some(MemoryType::Working),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
MemoryType::Episodic => "episodic",
MemoryType::Semantic => "semantic",
MemoryType::Procedural => "procedural",
MemoryType::Working => "working",
}
}
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Scope {
Private,
Shared,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Memory {
pub id: Ulid,
pub memory_type: MemoryType,
pub vector: Vec<f32>,
pub content: String,
pub metadata: Map<String, Value>,
pub agent_id: String,
pub session_id: Option<String>,
pub scope: Scope,
pub created_at: i64,
pub accessed_at: i64,
pub access_count: u32,
pub importance: f32,
pub ttl_secs: Option<i64>,
}
impl Memory {
pub fn new(content: impl Into<String>, memory_type: MemoryType, vector: Vec<f32>) -> Self {
let now = now_secs();
Memory {
id: Ulid::new(),
memory_type,
vector,
content: content.into(),
metadata: Map::new(),
agent_id: "default".to_string(),
session_id: None,
scope: Scope::Private,
created_at: now,
accessed_at: now,
access_count: 0,
importance: 0.5,
ttl_secs: None,
}
}
pub fn with_agent(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = agent_id.into();
self
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_importance(mut self, importance: f32) -> Self {
self.importance = importance.clamp(0.0, 1.0);
self
}
pub fn with_scope(mut self, scope: Scope) -> Self {
self.scope = scope;
self
}
pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
self.ttl_secs = Some(ttl_secs);
self
}
pub fn with_meta(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn is_expired(&self, now: i64) -> bool {
match self.ttl_secs {
Some(ttl) => now - self.created_at >= ttl,
None => false,
}
}
pub fn scaffold_manifest(dimensions: usize) -> Self {
let content = format!(
"MNEMO MANIFEST (scaffold) — Fresh {}-dimensional database created \
by `mnemo init`. This is a placeholder. Replace it with a memory \
that records your project's embedder (name, dimensions, normalize), \
default agent_id, and metadata conventions; keep the metadata keys \
`area=\"onboarding\"` and `topic=\"manifest\"` so `mnemo about` \
continues to surface it as the headline orientation point. Drop \
`metadata.scaffold` once you've replaced this entry so tooling \
knows the manifest has been curated.",
dimensions
);
let mut metadata = Map::new();
metadata.insert("area".into(), Value::String("onboarding".into()));
metadata.insert("topic".into(), Value::String("manifest".into()));
metadata.insert("scaffold".into(), Value::Bool(true));
metadata.insert(
"engine_version".into(),
Value::String(env!("CARGO_PKG_VERSION").into()),
);
metadata.insert(
"dimensions".into(),
Value::Number(serde_json::Number::from(dimensions as u64)),
);
let mut vector = vec![0.0_f32; dimensions];
if dimensions > 0 {
vector[0] = 1.0;
}
let now = now_secs();
Memory {
id: Ulid::new(),
memory_type: MemoryType::Semantic,
vector,
content,
metadata,
agent_id: "mnemo".to_string(),
session_id: None,
scope: Scope::Shared,
created_at: now,
accessed_at: now,
access_count: 0,
importance: 1.0,
ttl_secs: None,
}
}
}
pub const MNEMO_META_NS: &str = "_mnemo.";
pub const MNEMO_META_KEY: &str = "_mnemo.key";
pub const MNEMO_META_SUPERSEDES: &str = "_mnemo.supersedes";
pub const MNEMO_META_SUPERSEDED_BY: &str = "_mnemo.superseded_by";
pub const MNEMO_META_INVALID_AT: &str = "_mnemo.invalid_at";
pub const MNEMO_META_UPDATED_AT: &str = "_mnemo.updated_at";
pub const MNEMO_META_KIND: &str = "_mnemo.kind";
pub const MNEMO_META_THREAD: &str = "_mnemo.thread";
pub const MNEMO_META_SEQ: &str = "_mnemo.seq";
pub const MNEMO_META_ROLE: &str = "_mnemo.role";
pub const MNEMO_META_TURN_ID: &str = "_mnemo.turn_id";
pub const MNEMO_META_PART: &str = "_mnemo.part";
pub const MNEMO_META_SHA256: &str = "_mnemo.sha256";
pub const MNEMO_META_LENGTH: &str = "_mnemo.length";
pub const MNEMO_META_KIND_TURN: &str = "turn";
pub const THREAD_TURN_CHUNK_BYTES: usize = 8 * 1024;
pub const THREAD_TOOL_RESULT_TRUNCATE_BYTES: usize = 16 * 1024;
pub const MNEMO_META_SECTION: &str = "_mnemo.section";
pub const MNEMO_META_STATUS: &str = "_mnemo.status";
pub const MNEMO_META_STATUS_HISTORY: &str = "_mnemo.status_history";
pub const MNEMO_META_REFS: &str = "_mnemo.refs";
pub const MNEMO_META_KIND_ESSENCE: &str = "essence";
pub const ESSENCE_TIMELINE_MAX_BYTES: usize = 600;
pub const ESSENCE_SECTION_MAX_BYTES: usize = 280;
pub const ESSENCE_DEFAULT_RENDER_BYTES: usize = 6144;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EssenceSection {
Intent,
Conventions,
Decisions,
State,
Open,
Next,
Lessons,
Timeline,
}
impl EssenceSection {
pub fn as_str(&self) -> &'static str {
match self {
EssenceSection::Intent => "intent",
EssenceSection::Conventions => "conventions",
EssenceSection::Decisions => "decisions",
EssenceSection::State => "state",
EssenceSection::Open => "open",
EssenceSection::Next => "next",
EssenceSection::Lessons => "lessons",
EssenceSection::Timeline => "timeline",
}
}
pub fn parse(s: &str) -> Option<EssenceSection> {
match s.to_ascii_lowercase().as_str() {
"intent" => Some(EssenceSection::Intent),
"conventions" => Some(EssenceSection::Conventions),
"decisions" => Some(EssenceSection::Decisions),
"state" => Some(EssenceSection::State),
"open" => Some(EssenceSection::Open),
"next" => Some(EssenceSection::Next),
"lessons" => Some(EssenceSection::Lessons),
"timeline" => Some(EssenceSection::Timeline),
_ => None,
}
}
pub fn render_order() -> &'static [EssenceSection] {
&[
EssenceSection::Intent,
EssenceSection::Conventions,
EssenceSection::Decisions,
EssenceSection::State,
EssenceSection::Open,
EssenceSection::Next,
EssenceSection::Lessons,
EssenceSection::Timeline,
]
}
pub fn has_status_lifecycle(&self) -> bool {
!matches!(self, EssenceSection::Timeline)
}
pub fn item_max_bytes(&self) -> usize {
match self {
EssenceSection::Timeline => ESSENCE_TIMELINE_MAX_BYTES,
_ => ESSENCE_SECTION_MAX_BYTES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum EssenceScope {
Thread(String),
Project,
}
pub mod status {
pub const OPEN: &str = "open";
pub const RESOLVED: &str = "resolved";
}
pub const TASK_TRANSITIONS: &[(&str, &str)] = &[
("queued", "active"),
("active", "ready"),
("active", "blocked"),
("blocked", "active"),
("ready", "done"),
];
pub const FINDING_TRANSITIONS: &[(&str, &str)] = &[
("open", "accepted"),
("open", "deferred"),
("open", "rejected"),
("open", "needs-user"),
("accepted", "verified"),
("deferred", "verified"),
("needs-user", "verified"),
];
pub const ESSENCE_TRANSITIONS: &[(&str, &str)] = &[(status::OPEN, status::RESOLVED)];
pub const MNEMO_META_CONTINUES: &str = "_mnemo.continues";
pub const MNEMO_META_KIND_THREAD_LINK: &str = "thread_link";
pub const CONTINUATION_TIMELINE_ENTRIES: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TurnPolicy {
#[default]
Archive,
ExpireAfter(i64),
Drop,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EssenceOpKind {
Add,
AppendTimeline,
Update,
Resolve,
Remove,
}
#[derive(Debug, Clone)]
pub struct EssenceOp {
pub kind: EssenceOpKind,
pub scope: Option<EssenceScope>,
pub section: Option<EssenceSection>,
pub id: Option<Ulid>,
pub text: Option<String>,
pub refs: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct KeyedMemory {
pub memory: Memory,
pub key: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct CommitBatch {
pub facts: Vec<KeyedMemory>,
pub essence_ops: Vec<EssenceOp>,
pub turns: Vec<ThreadTurn>,
pub thread_id: Option<String>,
pub turn_policy: TurnPolicy,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommitReport {
pub facts_added: usize,
pub facts_superseded: usize,
pub facts_unchanged: usize,
pub essence_ops_applied: usize,
pub essence_ops_unchanged: usize,
pub turns_added: usize,
pub turns_skipped: usize,
pub turns_truncated: usize,
pub turns_chunked: usize,
}
#[derive(Debug, Clone, Default)]
pub struct MemoryPatch {
pub content: Option<String>,
pub vector: Option<Vec<f32>>,
pub importance: Option<f32>,
pub memory_type: Option<MemoryType>,
pub metadata: Option<Map<String, Value>>,
pub ttl_secs: Option<Option<i64>>,
}
#[derive(Debug, Clone)]
pub struct ThreadTurn {
pub turn_id: String,
pub role: String,
pub content: String,
pub timestamp: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportedTurn {
pub turn_id: String,
pub role: String,
pub content: String,
pub timestamp: i64,
pub sha256: Option<String>,
pub original_length: Option<u64>,
}
impl ExportedTurn {
pub fn to_json(&self) -> serde_json::Value {
let mut obj = serde_json::json!({
"turn_id": self.turn_id,
"role": self.role,
"content": self.content,
"timestamp": self.timestamp,
});
if let Some(sha) = &self.sha256 {
obj["sha256"] = serde_json::Value::String(sha.clone());
}
if let Some(len) = self.original_length {
obj["original_length"] = serde_json::Value::Number(len.into());
}
obj
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IngestReport {
pub added_records: usize,
pub skipped_existing_turns: usize,
pub truncated_turns: usize,
pub chunked_turns: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum KeyScope {
Private(String),
Shared,
}
impl KeyScope {
pub const DEFAULT_AGENT: &'static str = "default";
pub fn private_default() -> Self {
KeyScope::Private(Self::DEFAULT_AGENT.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteOutcome {
Added,
Superseded,
Unchanged,
Updated,
}
#[derive(Debug, Clone)]
pub enum FilterTerm {
Exists(String),
Eq(String, Value),
In(String, Vec<Value>),
Range {
key: String,
min: Option<f64>,
max: Option<f64>,
},
}
#[derive(Debug, Clone, Default)]
pub struct Filter {
pub session_id: Option<String>,
pub terms: Vec<FilterTerm>,
}
impl Filter {
pub fn new_session(session: impl Into<String>) -> Self {
Self {
session_id: Some(session.into()),
terms: Vec::new(),
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session_id = Some(session.into());
self
}
pub fn metadata_exists(mut self, key: impl Into<String>) -> Self {
self.terms.push(FilterTerm::Exists(key.into()));
self
}
pub fn metadata_eq(mut self, key: impl Into<String>, value: Value) -> Self {
self.terms.push(FilterTerm::Eq(key.into(), value));
self
}
pub fn metadata_in(mut self, key: impl Into<String>, values: Vec<Value>) -> Self {
self.terms.push(FilterTerm::In(key.into(), values));
self
}
pub fn metadata_range(
mut self,
key: impl Into<String>,
min: Option<f64>,
max: Option<f64>,
) -> Self {
self.terms.push(FilterTerm::Range {
key: key.into(),
min,
max,
});
self
}
pub fn is_empty(&self) -> bool {
self.session_id.is_none() && self.terms.is_empty()
}
pub fn mentions_mnemo_meta(&self) -> bool {
self.terms.iter().any(|term| {
let key = match term {
FilterTerm::Exists(k) => k,
FilterTerm::Eq(k, _) => k,
FilterTerm::In(k, _) => k,
FilterTerm::Range { key, .. } => key,
};
key.starts_with(MNEMO_META_NS)
})
}
pub fn matches(&self, m: &Memory) -> bool {
if let Some(s) = &self.session_id {
match &m.session_id {
Some(ms) if ms == s => {}
_ => return false,
}
}
for term in &self.terms {
if !term_matches(term, &m.metadata) {
return false;
}
}
true
}
}
fn term_matches(term: &FilterTerm, metadata: &Map<String, Value>) -> bool {
match term {
FilterTerm::Exists(key) => metadata.contains_key(key),
FilterTerm::Eq(key, v) => metadata.get(key).is_some_and(|got| values_equal(got, v)),
FilterTerm::In(key, options) => metadata
.get(key)
.is_some_and(|got| options.iter().any(|opt| values_equal(got, opt))),
FilterTerm::Range { key, min, max } => metadata
.get(key)
.and_then(|v| v.as_f64())
.is_some_and(|n| min.map_or(true, |m_| n >= m_) && max.map_or(true, |m_| n <= m_)),
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => match (x.as_f64(), y.as_f64()) {
(Some(xf), Some(yf)) => (xf - yf).abs() < f64::EPSILON,
_ => x == y,
},
(x, y) => x == y,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Metric {
Cosine,
L2,
Dot,
}
pub fn similarity(metric: Metric, a: &[f32], b: &[f32]) -> f32 {
match metric {
Metric::Cosine => cosine(a, b),
Metric::Dot => dot(a, b),
Metric::L2 => {
let d = euclidean(a, b);
1.0 / (1.0 + d)
}
}
}
fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
pub(crate) fn cosine(a: &[f32], b: &[f32]) -> f32 {
let na = dot(a, a).sqrt();
let nb = dot(b, b).sqrt();
if na == 0.0 || nb == 0.0 {
0.0
} else {
dot(a, b) / (na * nb)
}
}
fn euclidean(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b)
.map(|(x, y)| (x - y) * (x - y))
.sum::<f32>()
.sqrt()
}
#[derive(Clone, Copy, Debug)]
pub struct ScoreWeights {
pub alpha: f32,
pub beta: f32,
pub gamma: f32,
pub delta: f32,
pub half_life_secs: f32,
}
impl Default for ScoreWeights {
fn default() -> Self {
Self {
alpha: 1.0,
beta: 0.3,
gamma: 0.2,
delta: 0.1,
half_life_secs: 7.0 * 24.0 * 3600.0,
}
}
}
const USAGE_SAT_K: f32 = 5.0;
impl ScoreWeights {
pub fn score(&self, similarity: f32, age_secs: f32, importance: f32, access_count: u32) -> f32 {
let recency = (-std::f32::consts::LN_2 * age_secs.max(0.0) / self.half_life_secs).exp();
let usage = (access_count as f32 / USAGE_SAT_K).tanh();
let gate = similarity.max(0.0);
self.alpha * similarity
+ gate * (self.beta * recency + self.gamma * (importance - 0.5) + self.delta * usage)
}
}