Skip to main content

conch_core/
memory.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Fact {
6    pub subject: String,
7    pub relation: String,
8    pub object: String,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Episode {
13    pub text: String,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum MemoryKind {
18    Fact(Fact),
19    Episode(Episode),
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct MemoryRecord {
24    pub id: i64,
25    pub kind: MemoryKind,
26    pub strength: f64,
27    pub embedding: Option<Vec<f32>>,
28    pub created_at: DateTime<Utc>,
29    pub last_accessed_at: DateTime<Utc>,
30    pub access_count: i64,
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub tags: Vec<String>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub source: Option<String>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub session_id: Option<String>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub channel: Option<String>,
39    #[serde(default = "default_importance")]
40    pub importance: f64,
41    #[serde(default = "default_namespace")]
42    pub namespace: String,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub checksum: Option<String>,
45}
46
47fn default_importance() -> f64 {
48    0.5
49}
50
51fn default_namespace() -> String {
52    "default".to_string()
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MemoryStats {
57    pub total_memories: i64,
58    pub total_facts: i64,
59    pub total_episodes: i64,
60    pub avg_strength: f64,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ExportData {
65    pub memories: Vec<MemoryRecord>,
66}
67
68// ── Audit log types ─────────────────────────────────────────
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AuditEntry {
72    pub id: i64,
73    pub timestamp: DateTime<Utc>,
74    pub action: String,
75    pub memory_id: Option<i64>,
76    pub actor: String,
77    pub details_json: Option<String>,
78}
79
80// ── Verification types ──────────────────────────────────────
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct VerifyResult {
84    pub total_checked: usize,
85    pub valid: usize,
86    pub corrupted: Vec<CorruptedMemory>,
87    pub missing_checksum: usize,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct CorruptedMemory {
92    pub id: i64,
93    pub expected: String,
94    pub actual: String,
95}
96
97/// Result of a remember operation, indicating whether the memory was newly
98/// created or deduplicated against an existing memory.
99#[derive(Debug, Clone, Serialize)]
100pub enum RememberResult {
101    /// A new memory was created.
102    Created(MemoryRecord),
103    /// A duplicate was detected (cosine similarity > threshold).
104    /// The existing memory was reinforced instead of creating a new one.
105    Duplicate {
106        existing: MemoryRecord,
107        similarity: f32,
108    },
109    /// An existing fact with the same subject+relation was updated (upsert).
110    Updated(MemoryRecord),
111}
112
113impl RememberResult {
114    /// Returns the memory record regardless of whether it was new, duplicate, or updated.
115    pub fn memory(&self) -> &MemoryRecord {
116        match self {
117            RememberResult::Created(m) => m,
118            RememberResult::Duplicate { existing, .. } => existing,
119            RememberResult::Updated(m) => m,
120        }
121    }
122
123    /// Returns true if this was a duplicate detection.
124    pub fn is_duplicate(&self) -> bool {
125        matches!(self, RememberResult::Duplicate { .. })
126    }
127
128    /// Returns true if this was an upsert (existing fact updated).
129    pub fn is_updated(&self) -> bool {
130        matches!(self, RememberResult::Updated(_))
131    }
132}
133
134/// A node in a graph traversal result, representing a memory at a certain hop distance.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct GraphNode {
137    pub memory: MemoryRecord,
138    /// How many hops from the query subject (0 = direct match).
139    pub depth: usize,
140    /// The entity (subject or object) that connects this node to the previous hop.
141    pub connected_via: String,
142}
143
144/// Provenance information for a single memory.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ProvenanceInfo {
147    pub memory: MemoryRecord,
148    pub created_at: String,
149    pub last_accessed_at: String,
150    pub access_count: i64,
151    pub strength: f64,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub source: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub session_id: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub channel: Option<String>,
158    /// 1-hop related facts for context.
159    pub related: Vec<GraphNode>,
160}
161
162impl MemoryRecord {
163    pub fn text_for_embedding(&self) -> String {
164        match &self.kind {
165            MemoryKind::Fact(f) => format!("{} {} {}", f.subject, f.relation, f.object),
166            MemoryKind::Episode(e) => e.text.clone(),
167        }
168    }
169
170    pub fn subject(&self) -> Option<&str> {
171        match &self.kind {
172            MemoryKind::Fact(f) => Some(&f.subject),
173            MemoryKind::Episode(_) => None,
174        }
175    }
176
177    /// Returns the object of this memory if it's a Fact.
178    pub fn object(&self) -> Option<&str> {
179        match &self.kind {
180            MemoryKind::Fact(f) => Some(&f.object),
181            MemoryKind::Episode(_) => None,
182        }
183    }
184}