1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MemoryTier {
12 Session,
14 Project,
16 Global,
18}
19
20impl MemoryTier {
21 pub fn table_prefix(&self) -> &'static str {
23 match self {
24 MemoryTier::Session => "session",
25 MemoryTier::Project => "project",
26 MemoryTier::Global => "global",
27 }
28 }
29}
30
31impl std::fmt::Display for MemoryTier {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 MemoryTier::Session => write!(f, "session"),
35 MemoryTier::Project => write!(f, "project"),
36 MemoryTier::Global => write!(f, "global"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct MemoryChunk {
44 pub id: String,
45 pub content: String,
46 pub tier: MemoryTier,
47 pub session_id: Option<String>,
48 pub project_id: Option<String>,
49 pub source: String, pub source_path: Option<String>,
52 pub source_mtime: Option<i64>,
53 pub source_size: Option<i64>,
54 pub source_hash: Option<String>,
55 pub created_at: DateTime<Utc>,
56 pub token_count: i64,
57 pub metadata: Option<serde_json::Value>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct MemorySearchResult {
63 pub chunk: MemoryChunk,
64 pub similarity: f64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct MemoryConfig {
70 pub max_chunks: i64,
72 pub chunk_size: i64,
74 pub retrieval_k: i64,
76 pub auto_cleanup: bool,
78 pub session_retention_days: i64,
80 pub token_budget: i64,
82 pub chunk_overlap: i64,
84}
85
86impl Default for MemoryConfig {
87 fn default() -> Self {
88 Self {
89 max_chunks: 10_000,
90 chunk_size: 512,
91 retrieval_k: 5,
92 auto_cleanup: true,
93 session_retention_days: 30,
94 token_budget: 5000,
95 chunk_overlap: 64,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct MemoryStats {
103 pub total_chunks: i64,
105 pub session_chunks: i64,
107 pub project_chunks: i64,
109 pub global_chunks: i64,
111 pub total_bytes: i64,
113 pub session_bytes: i64,
115 pub project_bytes: i64,
117 pub global_bytes: i64,
119 pub file_size: i64,
121 pub last_cleanup: Option<DateTime<Utc>>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct MemoryContext {
128 pub current_session: Vec<MemoryChunk>,
130 pub relevant_history: Vec<MemoryChunk>,
132 pub project_facts: Vec<MemoryChunk>,
134 pub total_tokens: i64,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct MemoryRetrievalMeta {
141 pub used: bool,
142 pub chunks_total: usize,
143 pub session_chunks: usize,
144 pub history_chunks: usize,
145 pub project_fact_chunks: usize,
146 pub score_min: Option<f64>,
147 pub score_max: Option<f64>,
148}
149
150impl MemoryContext {
151 pub fn format_for_injection(&self) -> String {
153 let mut parts = Vec::new();
154
155 if !self.current_session.is_empty() {
156 parts.push("<current_session>".to_string());
157 for chunk in &self.current_session {
158 parts.push(format!("- {}", chunk.content));
159 }
160 parts.push("</current_session>".to_string());
161 }
162
163 if !self.relevant_history.is_empty() {
164 parts.push("<relevant_history>".to_string());
165 for chunk in &self.relevant_history {
166 parts.push(format!("- {}", chunk.content));
167 }
168 parts.push("</relevant_history>".to_string());
169 }
170
171 if !self.project_facts.is_empty() {
172 parts.push("<project_facts>".to_string());
173 for chunk in &self.project_facts {
174 parts.push(format!("- {}", chunk.content));
175 }
176 parts.push("</project_facts>".to_string());
177 }
178
179 if parts.is_empty() {
180 String::new()
181 } else {
182 format!("<memory_context>\n{}\n</memory_context>", parts.join("\n"))
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct StoreMessageRequest {
190 pub content: String,
191 pub tier: MemoryTier,
192 pub session_id: Option<String>,
193 pub project_id: Option<String>,
194 pub source: String,
195 pub source_path: Option<String>,
197 pub source_mtime: Option<i64>,
198 pub source_size: Option<i64>,
199 pub source_hash: Option<String>,
200 pub metadata: Option<serde_json::Value>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ProjectMemoryStats {
206 pub project_id: String,
207 pub project_chunks: i64,
209 pub project_bytes: i64,
210 pub file_index_chunks: i64,
212 pub file_index_bytes: i64,
213 pub indexed_files: i64,
215 pub last_indexed_at: Option<DateTime<Utc>>,
217 pub last_total_files: Option<i64>,
219 pub last_processed_files: Option<i64>,
220 pub last_indexed_files: Option<i64>,
221 pub last_skipped_files: Option<i64>,
222 pub last_errors: Option<i64>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ClearFileIndexResult {
227 pub chunks_deleted: i64,
228 pub bytes_estimated: i64,
229 pub did_vacuum: bool,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "snake_case")]
234pub enum MemoryImportFormat {
235 Directory,
236 Openclaw,
237}
238
239impl std::fmt::Display for MemoryImportFormat {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 MemoryImportFormat::Directory => write!(f, "directory"),
243 MemoryImportFormat::Openclaw => write!(f, "openclaw"),
244 }
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct MemoryImportRequest {
250 pub root_path: String,
251 pub format: MemoryImportFormat,
252 pub tier: MemoryTier,
253 pub session_id: Option<String>,
254 pub project_id: Option<String>,
255 pub sync_deletes: bool,
256}
257
258#[derive(Debug, Clone, Default, Serialize, Deserialize)]
259pub struct MemoryImportStats {
260 pub discovered_files: usize,
261 pub files_processed: usize,
262 pub indexed_files: usize,
263 pub skipped_files: usize,
264 pub deleted_files: usize,
265 pub chunks_created: usize,
266 pub errors: usize,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct MemoryImportProgress {
271 pub files_processed: usize,
272 pub total_files: usize,
273 pub indexed_files: usize,
274 pub skipped_files: usize,
275 pub deleted_files: usize,
276 pub errors: usize,
277 pub chunks_created: usize,
278 pub current_file: String,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct SearchMemoryRequest {
284 pub query: String,
285 pub tier: Option<MemoryTier>,
286 pub project_id: Option<String>,
287 pub session_id: Option<String>,
288 pub limit: Option<i64>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct EmbeddingHealth {
294 pub status: String,
296 pub reason: Option<String>,
298}
299
300#[derive(Error, Debug)]
302pub enum MemoryError {
303 #[error("Database error: {0}")]
304 Database(#[from] rusqlite::Error),
305
306 #[error("IO error: {0}")]
307 Io(#[from] std::io::Error),
308
309 #[error("Serialization error: {0}")]
310 Serialization(#[from] serde_json::Error),
311
312 #[error("Embedding error: {0}")]
313 Embedding(String),
314
315 #[error("Chunking error: {0}")]
316 Chunking(String),
317
318 #[error("Invalid configuration: {0}")]
319 InvalidConfig(String),
320
321 #[error("Not found: {0}")]
322 NotFound(String),
323
324 #[error("Tokenization error: {0}")]
325 Tokenization(String),
326
327 #[error("Lock error: {0}")]
328 Lock(String),
329}
330
331impl From<String> for MemoryError {
332 fn from(err: String) -> Self {
333 MemoryError::InvalidConfig(err)
334 }
335}
336
337impl From<&str> for MemoryError {
338 fn from(err: &str) -> Self {
339 MemoryError::InvalidConfig(err.to_string())
340 }
341}
342
343impl serde::Serialize for MemoryError {
345 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346 where
347 S: serde::Serializer,
348 {
349 serializer.serialize_str(&self.to_string())
350 }
351}
352
353pub type MemoryResult<T> = Result<T, MemoryError>;
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct CleanupLogEntry {
358 pub id: String,
359 pub cleanup_type: String,
360 pub tier: MemoryTier,
361 pub project_id: Option<String>,
362 pub session_id: Option<String>,
363 pub chunks_deleted: i64,
364 pub bytes_reclaimed: i64,
365 pub created_at: DateTime<Utc>,
366}
367
368pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
370
371pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
373
374pub const MAX_CHUNK_LENGTH: usize = 4000;
376
377pub const MIN_CHUNK_LENGTH: usize = 50;
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct GlobalMemoryRecord {
383 pub id: String,
384 pub user_id: String,
385 pub source_type: String,
386 pub content: String,
387 pub content_hash: String,
388 pub run_id: String,
389 pub session_id: Option<String>,
390 pub message_id: Option<String>,
391 pub tool_name: Option<String>,
392 pub project_tag: Option<String>,
393 pub channel_tag: Option<String>,
394 pub host_tag: Option<String>,
395 pub metadata: Option<serde_json::Value>,
396 pub provenance: Option<serde_json::Value>,
397 pub redaction_status: String,
398 pub redaction_count: u32,
399 pub visibility: String,
400 pub demoted: bool,
401 pub score_boost: f64,
402 pub created_at_ms: u64,
403 pub updated_at_ms: u64,
404 pub expires_at_ms: Option<u64>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct GlobalMemoryWriteResult {
409 pub id: String,
410 pub stored: bool,
411 pub deduped: bool,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct GlobalMemorySearchHit {
416 pub record: GlobalMemoryRecord,
417 pub score: f64,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
421#[serde(rename_all = "lowercase")]
422pub enum NodeType {
423 Directory,
424 File,
425}
426
427impl std::fmt::Display for NodeType {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 match self {
430 NodeType::Directory => write!(f, "directory"),
431 NodeType::File => write!(f, "file"),
432 }
433 }
434}
435
436impl std::str::FromStr for NodeType {
437 type Err = String;
438 fn from_str(s: &str) -> Result<Self, Self::Err> {
439 match s.to_lowercase().as_str() {
440 "directory" => Ok(NodeType::Directory),
441 "file" => Ok(NodeType::File),
442 _ => Err(format!("unknown node type: {}", s)),
443 }
444 }
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(rename_all = "UPPERCASE")]
449pub enum LayerType {
450 L0,
451 L1,
452 L2,
453}
454
455impl std::fmt::Display for LayerType {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 match self {
458 LayerType::L0 => write!(f, "L0"),
459 LayerType::L1 => write!(f, "L1"),
460 LayerType::L2 => write!(f, "L2"),
461 }
462 }
463}
464
465impl std::str::FromStr for LayerType {
466 type Err = String;
467 fn from_str(s: &str) -> Result<Self, Self::Err> {
468 match s.to_uppercase().as_str() {
469 "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
470 "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
471 "L2" | "L2_DETAIL" => Ok(LayerType::L2),
472 _ => Err(format!("unknown layer type: {}", s)),
473 }
474 }
475}
476
477impl LayerType {
478 pub fn default_tokens(&self) -> usize {
479 match self {
480 LayerType::L0 => 100,
481 LayerType::L1 => 2000,
482 LayerType::L2 => 4000,
483 }
484 }
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct RetrievalStep {
489 pub step_type: String,
490 pub description: String,
491 pub layer_accessed: Option<LayerType>,
492 pub nodes_evaluated: usize,
493 pub scores: std::collections::HashMap<String, f64>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct NodeVisit {
498 pub uri: String,
499 pub node_type: NodeType,
500 pub score: f64,
501 pub depth: usize,
502 pub layer_loaded: Option<LayerType>,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct RetrievalTrajectory {
507 pub id: String,
508 pub query: String,
509 pub root_uri: String,
510 pub steps: Vec<RetrievalStep>,
511 pub visited_nodes: Vec<NodeVisit>,
512 pub total_duration_ms: u64,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct RetrievalResult {
517 pub node_id: String,
518 pub uri: String,
519 pub content: String,
520 pub layer_type: LayerType,
521 pub score: f64,
522 pub trajectory: RetrievalTrajectory,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct MemoryNode {
527 pub id: String,
528 pub uri: String,
529 pub parent_uri: Option<String>,
530 pub node_type: NodeType,
531 pub created_at: DateTime<Utc>,
532 pub updated_at: DateTime<Utc>,
533 pub metadata: Option<serde_json::Value>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct MemoryLayer {
538 pub id: String,
539 pub node_id: String,
540 pub layer_type: LayerType,
541 pub content: String,
542 pub token_count: i64,
543 pub embedding_id: Option<String>,
544 pub created_at: DateTime<Utc>,
545 pub source_chunk_id: Option<String>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct TreeNode {
550 pub node: MemoryNode,
551 pub children: Vec<TreeNode>,
552 pub layer_summary: Option<LayerSummary>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct LayerSummary {
557 pub l0_preview: Option<String>,
558 pub l1_preview: Option<String>,
559 pub has_l2: bool,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct DirectoryListing {
564 pub uri: String,
565 pub nodes: Vec<MemoryNode>,
566 pub total_children: usize,
567 pub directories: Vec<MemoryNode>,
568 pub files: Vec<MemoryNode>,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct DistilledFact {
573 pub id: String,
574 pub distillation_id: String,
575 pub content: String,
576 pub category: FactCategory,
577 pub importance_score: f64,
578 pub source_message_ids: Vec<String>,
579 pub contradicts_fact_id: Option<String>,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
583#[serde(rename_all = "snake_case")]
584pub enum FactCategory {
585 UserPreference,
586 TaskOutcome,
587 Learning,
588 Fact,
589}
590
591impl std::fmt::Display for FactCategory {
592 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593 match self {
594 FactCategory::UserPreference => write!(f, "user_preference"),
595 FactCategory::TaskOutcome => write!(f, "task_outcome"),
596 FactCategory::Learning => write!(f, "learning"),
597 FactCategory::Fact => write!(f, "fact"),
598 }
599 }
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DistillationReport {
604 pub distillation_id: String,
605 pub session_id: String,
606 pub distilled_at: DateTime<Utc>,
607 pub facts_extracted: usize,
608 pub importance_threshold: f64,
609 pub user_memory_updated: bool,
610 pub agent_memory_updated: bool,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct SessionDistillation {
615 pub id: String,
616 pub session_id: String,
617 pub distilled_at: DateTime<Utc>,
618 pub input_token_count: i64,
619 pub output_memory_count: usize,
620 pub key_facts_extracted: usize,
621 pub importance_threshold: f64,
622}