1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use tandem_enterprise_contract::{
7 AccessDecision, AccessEffect, AccessPermission, DataBoundary, DataClass, ResourceKind,
8 ResourceRef, StrictTenantContext,
9};
10use tandem_orchestrator::{KnowledgeScope, KnowledgeTrustLevel};
11use thiserror::Error;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum MemoryTier {
17 Session,
19 Project,
21 Global,
23}
24
25impl MemoryTier {
26 pub fn table_prefix(&self) -> &'static str {
28 match self {
29 MemoryTier::Session => "session",
30 MemoryTier::Project => "project",
31 MemoryTier::Global => "global",
32 }
33 }
34}
35
36impl std::fmt::Display for MemoryTier {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 MemoryTier::Session => write!(f, "session"),
40 MemoryTier::Project => write!(f, "project"),
41 MemoryTier::Global => write!(f, "global"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct MemoryTenantScope {
49 pub org_id: String,
50 pub workspace_id: String,
51 pub deployment_id: Option<String>,
52}
53
54impl MemoryTenantScope {
55 pub fn local() -> Self {
56 Self {
57 org_id: "local".to_string(),
58 workspace_id: "local".to_string(),
59 deployment_id: None,
60 }
61 }
62
63 pub fn is_local(&self) -> bool {
67 self == &Self::local()
68 }
69}
70
71impl Default for MemoryTenantScope {
72 fn default() -> Self {
73 Self::local()
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MemoryChunk {
80 pub id: String,
81 pub content: String,
82 pub tier: MemoryTier,
83 pub session_id: Option<String>,
84 pub project_id: Option<String>,
85 pub source: String, pub source_path: Option<String>,
88 pub source_mtime: Option<i64>,
89 pub source_size: Option<i64>,
90 pub source_hash: Option<String>,
91 #[serde(default)]
92 pub tenant_scope: MemoryTenantScope,
93 pub created_at: DateTime<Utc>,
94 pub token_count: i64,
95 pub metadata: Option<serde_json::Value>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct MemorySearchResult {
101 pub chunk: MemoryChunk,
102 pub similarity: f64,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum GovernedReadMode {
108 LocalNoop,
109 GovernedStrict,
110}
111
112impl GovernedReadMode {
113 pub fn as_str(self) -> &'static str {
114 match self {
115 Self::LocalNoop => "local_noop",
116 Self::GovernedStrict => "governed_strict",
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct GovernedReadDecision {
123 pub allowed: bool,
124 pub reason: Option<String>,
125}
126
127impl GovernedReadDecision {
128 pub fn allow(reason: impl Into<String>) -> Self {
129 Self {
130 allowed: true,
131 reason: Some(reason.into()),
132 }
133 }
134
135 pub fn deny(reason: impl Into<String>) -> Self {
136 Self {
137 allowed: false,
138 reason: Some(reason.into()),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum GovernedReadEvidence {
145 SourceBinding,
146 TenantLocalMemory,
147}
148
149impl GovernedReadEvidence {
150 pub fn as_str(self) -> &'static str {
151 match self {
152 Self::SourceBinding => "source_binding",
153 Self::TenantLocalMemory => "tenant_local_memory",
154 }
155 }
156
157 fn requires_grant(self) -> bool {
158 matches!(self, Self::SourceBinding)
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct GovernedReadTarget {
164 pub resource_ref: ResourceRef,
165 pub data_class: DataClass,
166 pub source_binding_id: Option<String>,
167 pub source_object_id: Option<String>,
168 pub evidence: GovernedReadEvidence,
169}
170
171#[derive(Debug, Clone)]
173pub struct MemoryAccessFilter {
174 pub strict_context: Option<StrictTenantContext>,
175 pub now_ms: u64,
176 pub mode: GovernedReadMode,
177}
178
179impl MemoryAccessFilter {
180 pub fn strict(strict_context: StrictTenantContext, now_ms: u64) -> Self {
181 Self::governed(Some(strict_context), now_ms)
182 }
183
184 pub fn governed(strict_context: Option<StrictTenantContext>, now_ms: u64) -> Self {
185 Self {
186 strict_context,
187 now_ms,
188 mode: GovernedReadMode::GovernedStrict,
189 }
190 }
191
192 pub fn local_noop(now_ms: u64) -> Self {
193 Self {
194 strict_context: None,
195 now_ms,
196 mode: GovernedReadMode::LocalNoop,
197 }
198 }
199
200 pub fn allows_chunk(&self, chunk: &MemoryChunk) -> bool {
201 self.decision_for_chunk(chunk).allowed
202 }
203
204 pub fn allows_source_target(&self, target: &MemorySourceAccessTarget) -> bool {
205 self.decision_for_source_target(target).allowed
206 }
207
208 pub fn allows_global_record(&self, record: &GlobalMemoryRecord) -> bool {
209 self.decision_for_global_record(record).allowed
210 }
211
212 pub fn decision_for_chunk(&self, chunk: &MemoryChunk) -> GovernedReadDecision {
213 match governed_read_target_from_chunk(chunk) {
214 Ok(target) => self.decision_for_target(&target),
215 Err(reason) => self.decision_for_missing_target(reason),
216 }
217 }
218
219 pub fn decision_for_global_record(&self, record: &GlobalMemoryRecord) -> GovernedReadDecision {
220 match governed_read_target_from_global_record(record, self.strict_context.as_ref()) {
221 Ok(target) => self.decision_for_target(&target),
222 Err(reason) => self.decision_for_missing_target(reason),
223 }
224 }
225
226 pub fn decision_for_source_target(
227 &self,
228 target: &MemorySourceAccessTarget,
229 ) -> GovernedReadDecision {
230 self.decision_for_target(&GovernedReadTarget {
231 resource_ref: target.resource_ref.clone(),
232 data_class: target.data_class,
233 source_binding_id: target.source_binding_id.clone(),
234 source_object_id: target.source_object_id.clone(),
235 evidence: GovernedReadEvidence::SourceBinding,
236 })
237 }
238
239 fn decision_for_missing_target(&self, reason: &'static str) -> GovernedReadDecision {
240 if self.mode == GovernedReadMode::LocalNoop {
241 GovernedReadDecision::allow("local_noop")
242 } else {
243 GovernedReadDecision::deny(reason)
244 }
245 }
246
247 fn decision_for_target(&self, target: &GovernedReadTarget) -> GovernedReadDecision {
248 if self.mode == GovernedReadMode::LocalNoop {
249 return GovernedReadDecision::allow("local_noop");
250 }
251
252 let Some(strict_context) = self.strict_context.as_ref() else {
253 return GovernedReadDecision::deny("missing_strict_projection");
254 };
255
256 let effective_boundary =
257 effective_data_boundary_for_governed_read(strict_context, self.now_ms);
258 if !effective_boundary.allows(target.data_class) {
259 return GovernedReadDecision::deny("data_class_denied_by_boundary");
260 }
261
262 if strict_context.is_expired_at(self.now_ms) {
263 return GovernedReadDecision::deny("context_expired");
264 }
265
266 if !target.evidence.requires_grant() {
267 if target.resource_ref.organization_id != strict_context.tenant_context.org_id
268 || target.resource_ref.workspace_id != strict_context.tenant_context.workspace_id
269 {
270 return GovernedReadDecision::deny("tenant_scope_mismatch");
271 }
272 if strict_context
273 .resource_scope
274 .explicitly_denies(&target.resource_ref)
275 {
276 return GovernedReadDecision::deny("resource_explicitly_denied_by_scope");
277 }
278 if !strict_context.resource_scope.contains(&target.resource_ref) {
279 return GovernedReadDecision::deny("resource_outside_projected_scope");
280 }
281 return GovernedReadDecision::allow("tenant_local_memory_allowed");
282 }
283
284 let evaluation = strict_context
285 .clone()
286 .with_data_boundary(effective_boundary)
287 .evaluate_access(
288 &target.resource_ref,
289 AccessPermission::Read,
290 target.data_class,
291 self.now_ms,
292 );
293 if evaluation.decision == AccessDecision::Allow {
294 GovernedReadDecision::allow(evaluation.reason)
295 } else {
296 GovernedReadDecision::deny(evaluation.reason)
297 }
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct MemorySourceAccessTarget {
303 pub resource_ref: ResourceRef,
304 pub data_class: DataClass,
305 pub source_binding_id: Option<String>,
306 pub source_object_id: Option<String>,
307}
308
309impl MemorySourceAccessTarget {
310 pub fn from_chunk(chunk: &MemoryChunk) -> Option<Self> {
311 Self::from_metadata(chunk.metadata.as_ref())
312 }
313
314 pub fn from_metadata(metadata: Option<&serde_json::Value>) -> Option<Self> {
315 let binding = metadata?.get("enterprise_source_binding")?;
316 let resource_ref = serde_json::from_value(binding.get("resource_ref")?.clone()).ok()?;
317 let data_class = serde_json::from_value(binding.get("data_class")?.clone()).ok()?;
318 Some(Self {
319 resource_ref,
320 data_class,
321 source_binding_id: binding
322 .get("binding_id")
323 .and_then(serde_json::Value::as_str)
324 .map(ToOwned::to_owned),
325 source_object_id: binding
326 .get("source_object_id")
327 .and_then(serde_json::Value::as_str)
328 .map(ToOwned::to_owned),
329 })
330 }
331}
332
333pub fn effective_data_boundary_for_governed_read(
334 strict_context: &StrictTenantContext,
335 now_ms: u64,
336) -> DataBoundary {
337 if strict_context.data_boundary.is_unrestricted() {
338 let mut grant_data_classes = Vec::new();
339 for grant in strict_context.grants.iter().filter(|grant| {
340 grant.effect == AccessEffect::Allow
341 && !grant.is_expired_at(now_ms)
342 && grant.has_permission(AccessPermission::Read)
343 }) {
344 for data_class in &grant.data_classes {
345 if !grant_data_classes.contains(data_class) {
346 grant_data_classes.push(*data_class);
347 }
348 }
349 }
350 if grant_data_classes.is_empty() {
351 DataBoundary::governed_default()
352 } else {
353 DataBoundary::allow(grant_data_classes)
354 }
355 } else {
356 strict_context.data_boundary.clone()
357 }
358}
359
360fn governed_read_target_from_chunk(
361 chunk: &MemoryChunk,
362) -> Result<GovernedReadTarget, &'static str> {
363 if let Some(target) = source_binding_target_from_metadata(chunk.metadata.as_ref())? {
364 return Ok(target);
365 }
366
367 let mut resource_ref = ResourceRef::new(
368 chunk.tenant_scope.org_id.clone(),
369 chunk.tenant_scope.workspace_id.clone(),
370 ResourceKind::MemorySpace,
371 memory_chunk_resource_id(chunk),
372 );
373 if let Some(project_id) = chunk.project_id.as_ref() {
374 resource_ref = resource_ref.with_project_id(project_id.clone());
375 }
376
377 Ok(GovernedReadTarget {
378 resource_ref,
379 data_class: data_class_from_metadata(chunk.metadata.as_ref())
380 .unwrap_or(DataClass::Internal),
381 source_binding_id: None,
382 source_object_id: None,
383 evidence: GovernedReadEvidence::TenantLocalMemory,
384 })
385}
386
387fn governed_read_target_from_global_record(
388 record: &GlobalMemoryRecord,
389 strict_context: Option<&StrictTenantContext>,
390) -> Result<GovernedReadTarget, &'static str> {
391 if let Some(target) = source_binding_target_from_metadata(record.metadata.as_ref())? {
392 return Ok(target);
393 }
394
395 let Some(strict_context) = strict_context else {
396 return Err("missing_strict_projection");
397 };
398
399 let mut resource_ref = ResourceRef::new(
400 strict_context.tenant_context.org_id.clone(),
401 strict_context.tenant_context.workspace_id.clone(),
402 ResourceKind::MemorySpace,
403 global_memory_record_resource_id(record),
404 );
405 if let Some(project_id) = record.project_tag.as_ref() {
406 resource_ref = resource_ref.with_project_id(project_id.clone());
407 }
408
409 Ok(GovernedReadTarget {
410 resource_ref,
411 data_class: data_class_from_metadata(record.metadata.as_ref())
412 .unwrap_or(DataClass::Internal),
413 source_binding_id: None,
414 source_object_id: None,
415 evidence: GovernedReadEvidence::TenantLocalMemory,
416 })
417}
418
419fn source_binding_target_from_metadata(
420 metadata: Option<&serde_json::Value>,
421) -> Result<Option<GovernedReadTarget>, &'static str> {
422 let Some(metadata) = metadata else {
423 return Ok(None);
424 };
425 let Some(binding) = metadata.get("enterprise_source_binding") else {
426 return if memory_metadata_is_connector_sourced(Some(metadata)) {
427 Err("missing_resource_ref")
428 } else {
429 Ok(None)
430 };
431 };
432
433 let resource_value = binding.get("resource_ref").ok_or("missing_resource_ref")?;
434 let data_class_value = binding.get("data_class").ok_or("missing_data_class")?;
435 let resource_ref =
436 serde_json::from_value(resource_value.clone()).map_err(|_| "missing_resource_ref")?;
437 let data_class =
438 serde_json::from_value(data_class_value.clone()).map_err(|_| "missing_data_class")?;
439
440 Ok(Some(GovernedReadTarget {
441 resource_ref,
442 data_class,
443 source_binding_id: binding
444 .get("binding_id")
445 .and_then(serde_json::Value::as_str)
446 .map(ToOwned::to_owned),
447 source_object_id: binding
448 .get("source_object_id")
449 .and_then(serde_json::Value::as_str)
450 .map(ToOwned::to_owned),
451 evidence: GovernedReadEvidence::SourceBinding,
452 }))
453}
454
455fn memory_metadata_is_connector_sourced(metadata: Option<&serde_json::Value>) -> bool {
456 metadata
457 .and_then(|value| value.get("memory_trust"))
458 .and_then(|value| value.get("label"))
459 .and_then(serde_json::Value::as_str)
460 .is_some_and(|label| label == "connector_sourced")
461}
462
463fn data_class_from_metadata(metadata: Option<&serde_json::Value>) -> Option<DataClass> {
464 metadata
465 .and_then(|value| value.get("classification"))
466 .and_then(serde_json::Value::as_str)
467 .and_then(data_class_from_label)
468}
469
470fn data_class_from_label(label: &str) -> Option<DataClass> {
471 match label.trim().to_ascii_lowercase().replace('-', "_").as_str() {
472 "public" => Some(DataClass::Public),
473 "internal" => Some(DataClass::Internal),
474 "confidential" => Some(DataClass::Confidential),
475 "restricted" => Some(DataClass::Restricted),
476 "executive" => Some(DataClass::Executive),
477 "credential" => Some(DataClass::Credential),
478 "regulated" => Some(DataClass::Regulated),
479 "customer_data" | "customer" => Some(DataClass::CustomerData),
480 "source_code" | "code" => Some(DataClass::SourceCode),
481 "financial_record" | "financial" | "finance" => Some(DataClass::FinancialRecord),
482 _ => None,
483 }
484}
485
486fn memory_chunk_resource_id(chunk: &MemoryChunk) -> String {
487 match chunk.tier {
488 MemoryTier::Session => chunk
489 .session_id
490 .as_ref()
491 .map(|session_id| format!("session:{session_id}"))
492 .unwrap_or_else(|| "session:default".to_string()),
493 MemoryTier::Project => chunk
494 .project_id
495 .as_ref()
496 .map(|project_id| format!("project:{project_id}"))
497 .unwrap_or_else(|| "project:default".to_string()),
498 MemoryTier::Global => chunk
499 .project_id
500 .as_ref()
501 .map(|project_id| format!("global:{project_id}"))
502 .unwrap_or_else(|| "global".to_string()),
503 }
504}
505
506fn global_memory_record_resource_id(record: &GlobalMemoryRecord) -> String {
507 if record.visibility.eq_ignore_ascii_case("shared") {
508 record
509 .project_tag
510 .as_ref()
511 .map(|project_id| format!("project:{project_id}"))
512 .unwrap_or_else(|| "project:default".to_string())
513 } else {
514 record
515 .session_id
516 .as_ref()
517 .map(|session_id| format!("session:{session_id}"))
518 .unwrap_or_else(|| "global".to_string())
519 }
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct MemoryConfig {
525 pub max_chunks: i64,
527 pub chunk_size: i64,
529 pub retrieval_k: i64,
531 pub auto_cleanup: bool,
533 pub session_retention_days: i64,
535 pub token_budget: i64,
537 pub chunk_overlap: i64,
539}
540
541impl Default for MemoryConfig {
542 fn default() -> Self {
543 Self {
544 max_chunks: 10_000,
545 chunk_size: 512,
546 retrieval_k: 5,
547 auto_cleanup: true,
548 session_retention_days: 30,
549 token_budget: 5000,
550 chunk_overlap: 64,
551 }
552 }
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct MemoryStats {
558 pub total_chunks: i64,
560 pub session_chunks: i64,
562 pub project_chunks: i64,
564 pub global_chunks: i64,
566 pub total_bytes: i64,
568 pub session_bytes: i64,
570 pub project_bytes: i64,
572 pub global_bytes: i64,
574 pub file_size: i64,
576 pub last_cleanup: Option<DateTime<Utc>>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct MemoryContext {
583 pub current_session: Vec<MemoryChunk>,
585 pub relevant_history: Vec<MemoryChunk>,
587 pub project_facts: Vec<MemoryChunk>,
589 pub total_tokens: i64,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct MemoryRetrievalMeta {
596 pub used: bool,
597 pub chunks_total: usize,
598 pub session_chunks: usize,
599 pub history_chunks: usize,
600 pub project_fact_chunks: usize,
601 pub score_min: Option<f64>,
602 pub score_max: Option<f64>,
603}
604
605impl MemoryContext {
606 pub fn format_for_injection(&self) -> String {
608 let mut parts = Vec::new();
609
610 if !self.current_session.is_empty() {
611 parts.push("<current_session>".to_string());
612 for chunk in &self.current_session {
613 parts.push(format!("- {}", chunk.content));
614 }
615 parts.push("</current_session>".to_string());
616 }
617
618 if !self.relevant_history.is_empty() {
619 parts.push("<relevant_history>".to_string());
620 for chunk in &self.relevant_history {
621 parts.push(format!("- {}", chunk.content));
622 }
623 parts.push("</relevant_history>".to_string());
624 }
625
626 if !self.project_facts.is_empty() {
627 parts.push("<project_facts>".to_string());
628 for chunk in &self.project_facts {
629 parts.push(format!("- {}", chunk.content));
630 }
631 parts.push("</project_facts>".to_string());
632 }
633
634 if parts.is_empty() {
635 String::new()
636 } else {
637 format!("<memory_context>\n{}\n</memory_context>", parts.join("\n"))
638 }
639 }
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct StoreMessageRequest {
645 pub content: String,
646 pub tier: MemoryTier,
647 pub session_id: Option<String>,
648 pub project_id: Option<String>,
649 pub source: String,
650 pub source_path: Option<String>,
652 pub source_mtime: Option<i64>,
653 pub source_size: Option<i64>,
654 pub source_hash: Option<String>,
655 #[serde(default)]
656 pub tenant_scope: MemoryTenantScope,
657 pub metadata: Option<serde_json::Value>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ProjectMemoryStats {
663 pub project_id: String,
664 pub project_chunks: i64,
666 pub project_bytes: i64,
667 pub file_index_chunks: i64,
669 pub file_index_bytes: i64,
670 pub indexed_files: i64,
672 pub last_indexed_at: Option<DateTime<Utc>>,
674 pub last_total_files: Option<i64>,
676 pub last_processed_files: Option<i64>,
677 pub last_indexed_files: Option<i64>,
678 pub last_skipped_files: Option<i64>,
679 pub last_errors: Option<i64>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct ClearFileIndexResult {
684 pub chunks_deleted: i64,
685 pub bytes_estimated: i64,
686 pub did_vacuum: bool,
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
690#[serde(rename_all = "snake_case")]
691pub enum MemoryImportFormat {
692 Directory,
693 Openclaw,
694}
695
696impl std::fmt::Display for MemoryImportFormat {
697 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698 match self {
699 MemoryImportFormat::Directory => write!(f, "directory"),
700 MemoryImportFormat::Openclaw => write!(f, "openclaw"),
701 }
702 }
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct MemoryImportRequest {
707 pub root_path: String,
708 pub format: MemoryImportFormat,
709 pub tier: MemoryTier,
710 pub session_id: Option<String>,
711 pub project_id: Option<String>,
712 #[serde(default)]
713 pub tenant_scope: MemoryTenantScope,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub source_binding: Option<MemoryImportSourceBinding>,
716 pub sync_deletes: bool,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub import_namespace: Option<String>,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct MemoryImportSourceBinding {
723 pub binding_id: String,
724 pub connector_id: String,
725 pub resource_ref: serde_json::Value,
726 pub data_class: String,
727 #[serde(default)]
728 pub require_review: bool,
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
732#[serde(rename_all = "snake_case")]
733pub enum SourceObjectLifecycleState {
734 Active,
735 Quarantined,
736 Tombstoned,
737 Deleted,
738 Rescoped,
739}
740
741impl SourceObjectLifecycleState {
742 pub fn as_str(self) -> &'static str {
743 match self {
744 Self::Active => "active",
745 Self::Quarantined => "quarantined",
746 Self::Tombstoned => "tombstoned",
747 Self::Deleted => "deleted",
748 Self::Rescoped => "rescoped",
749 }
750 }
751
752 pub fn parse(value: &str) -> Self {
753 match value.trim().to_ascii_lowercase().as_str() {
754 "quarantined" => Self::Quarantined,
755 "tombstoned" => Self::Tombstoned,
756 "deleted" => Self::Deleted,
757 "rescoped" => Self::Rescoped,
758 _ => Self::Active,
759 }
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
764pub struct SourceObjectLifecycleRecord {
765 pub source_object_id: String,
766 pub tenant_scope: MemoryTenantScope,
767 pub source_binding_id: String,
768 pub connector_id: String,
769 pub state: SourceObjectLifecycleState,
770 pub tier: MemoryTier,
771 pub session_id: Option<String>,
772 pub project_id: Option<String>,
773 pub import_namespace: String,
774 pub indexed_path: String,
775 pub native_object_id: String,
776 pub resource_ref: serde_json::Value,
777 pub data_class: String,
778 pub content_hash: Option<String>,
779 pub source_hash: Option<String>,
780 pub first_seen_at_ms: u64,
781 pub last_seen_at_ms: u64,
782 pub tombstoned_at_ms: Option<u64>,
783 pub metadata: Option<serde_json::Value>,
784}
785
786#[derive(Debug, Clone, Default, Serialize, Deserialize)]
787pub struct MemoryImportStats {
788 pub discovered_files: usize,
789 pub files_processed: usize,
790 pub indexed_files: usize,
791 pub skipped_files: usize,
792 pub deleted_files: usize,
793 pub chunks_created: usize,
794 pub errors: usize,
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize)]
798pub struct MemoryImportProgress {
799 pub files_processed: usize,
800 pub total_files: usize,
801 pub indexed_files: usize,
802 pub skipped_files: usize,
803 pub deleted_files: usize,
804 pub errors: usize,
805 pub chunks_created: usize,
806 pub current_file: String,
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize)]
811pub struct SearchMemoryRequest {
812 pub query: String,
813 pub tier: Option<MemoryTier>,
814 pub project_id: Option<String>,
815 pub session_id: Option<String>,
816 pub limit: Option<i64>,
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize)]
821pub struct EmbeddingHealth {
822 pub status: String,
824 pub reason: Option<String>,
826}
827
828#[derive(Error, Debug)]
830pub enum MemoryError {
831 #[error("Database error: {0}")]
832 Database(#[from] rusqlite::Error),
833
834 #[error("IO error: {0}")]
835 Io(#[from] std::io::Error),
836
837 #[error("Serialization error: {0}")]
838 Serialization(#[from] serde_json::Error),
839
840 #[error("Embedding error: {0}")]
841 Embedding(String),
842
843 #[error("Chunking error: {0}")]
844 Chunking(String),
845
846 #[error("Invalid configuration: {0}")]
847 InvalidConfig(String),
848
849 #[error("Tenant scope violation: {0}")]
850 TenantScopeViolation(String),
851
852 #[error("Not found: {0}")]
853 NotFound(String),
854
855 #[error("Tokenization error: {0}")]
856 Tokenization(String),
857
858 #[error("Lock error: {0}")]
859 Lock(String),
860}
861
862impl From<String> for MemoryError {
863 fn from(err: String) -> Self {
864 MemoryError::InvalidConfig(err)
865 }
866}
867
868impl From<&str> for MemoryError {
869 fn from(err: &str) -> Self {
870 MemoryError::InvalidConfig(err.to_string())
871 }
872}
873
874impl serde::Serialize for MemoryError {
876 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
877 where
878 S: serde::Serializer,
879 {
880 serializer.serialize_str(&self.to_string())
881 }
882}
883
884pub type MemoryResult<T> = Result<T, MemoryError>;
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct CleanupLogEntry {
889 pub id: String,
890 pub cleanup_type: String,
891 pub tier: MemoryTier,
892 pub project_id: Option<String>,
893 pub session_id: Option<String>,
894 pub chunks_deleted: i64,
895 pub bytes_reclaimed: i64,
896 pub created_at: DateTime<Utc>,
897}
898
899pub const DEFAULT_EMBEDDING_DIMENSION: usize = 384;
901
902pub const DEFAULT_EMBEDDING_MODEL: &str = "all-MiniLM-L6-v2";
904
905pub const MAX_CHUNK_LENGTH: usize = 4000;
907
908pub const MIN_CHUNK_LENGTH: usize = 50;
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
913pub struct GlobalMemoryRecord {
914 pub id: String,
915 pub user_id: String,
916 pub source_type: String,
917 pub content: String,
918 pub content_hash: String,
919 pub run_id: String,
920 pub session_id: Option<String>,
921 pub message_id: Option<String>,
922 pub tool_name: Option<String>,
923 pub project_tag: Option<String>,
924 pub channel_tag: Option<String>,
925 pub host_tag: Option<String>,
926 pub metadata: Option<serde_json::Value>,
927 pub provenance: Option<serde_json::Value>,
928 pub redaction_status: String,
929 pub redaction_count: u32,
930 pub visibility: String,
931 pub demoted: bool,
932 pub score_boost: f64,
933 pub created_at_ms: u64,
934 pub updated_at_ms: u64,
935 pub expires_at_ms: Option<u64>,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
939pub struct GlobalMemoryWriteResult {
940 pub id: String,
941 pub stored: bool,
942 pub deduped: bool,
943}
944
945#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct GlobalMemorySearchHit {
947 pub record: GlobalMemoryRecord,
948 pub score: f64,
949}
950
951#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
953#[serde(rename_all = "snake_case")]
954pub enum KnowledgeItemStatus {
955 #[default]
956 Working,
957 Promoted,
958 ApprovedDefault,
959 Deprecated,
960}
961
962impl std::fmt::Display for KnowledgeItemStatus {
963 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
964 match self {
965 Self::Working => write!(f, "working"),
966 Self::Promoted => write!(f, "promoted"),
967 Self::ApprovedDefault => write!(f, "approved_default"),
968 Self::Deprecated => write!(f, "deprecated"),
969 }
970 }
971}
972
973impl std::str::FromStr for KnowledgeItemStatus {
974 type Err = String;
975 fn from_str(s: &str) -> Result<Self, Self::Err> {
976 match s {
977 "working" => Ok(Self::Working),
978 "promoted" => Ok(Self::Promoted),
979 "approved_default" => Ok(Self::ApprovedDefault),
980 "deprecated" => Ok(Self::Deprecated),
981 other => Err(format!("unknown knowledge item status: {}", other)),
982 }
983 }
984}
985
986impl KnowledgeItemStatus {
987 pub fn as_trust_level(self) -> Option<KnowledgeTrustLevel> {
988 match self {
989 Self::Working => Some(KnowledgeTrustLevel::Working),
990 Self::Promoted => Some(KnowledgeTrustLevel::Promoted),
991 Self::ApprovedDefault => Some(KnowledgeTrustLevel::ApprovedDefault),
992 Self::Deprecated => None,
993 }
994 }
995
996 pub fn is_active(self) -> bool {
997 !matches!(self, Self::Deprecated)
998 }
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1003pub struct KnowledgeSpaceRecord {
1004 pub id: String,
1005 pub scope: KnowledgeScope,
1006 pub project_id: Option<String>,
1007 pub namespace: Option<String>,
1008 pub title: Option<String>,
1009 pub description: Option<String>,
1010 pub trust_level: KnowledgeTrustLevel,
1011 pub metadata: Option<serde_json::Value>,
1012 pub created_at_ms: u64,
1013 pub updated_at_ms: u64,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1018pub struct KnowledgeItemRecord {
1019 pub id: String,
1020 pub space_id: String,
1021 pub coverage_key: String,
1022 pub dedupe_key: String,
1023 pub item_type: String,
1024 pub title: String,
1025 pub summary: Option<String>,
1026 pub payload: serde_json::Value,
1027 pub trust_level: KnowledgeTrustLevel,
1028 pub status: KnowledgeItemStatus,
1029 pub run_id: Option<String>,
1030 pub artifact_refs: Vec<String>,
1031 pub source_memory_ids: Vec<String>,
1032 pub freshness_expires_at_ms: Option<u64>,
1033 pub metadata: Option<serde_json::Value>,
1034 pub created_at_ms: u64,
1035 pub updated_at_ms: u64,
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize)]
1040pub struct KnowledgeCoverageRecord {
1041 pub coverage_key: String,
1042 pub space_id: String,
1043 pub latest_item_id: Option<String>,
1044 pub latest_dedupe_key: Option<String>,
1045 pub last_seen_at_ms: u64,
1046 pub last_promoted_at_ms: Option<u64>,
1047 pub freshness_expires_at_ms: Option<u64>,
1048 pub metadata: Option<serde_json::Value>,
1049}
1050
1051#[derive(Debug, Clone, Serialize, Deserialize)]
1053pub struct KnowledgePromotionRequest {
1054 pub item_id: String,
1055 pub target_status: KnowledgeItemStatus,
1056 pub promoted_at_ms: u64,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub freshness_expires_at_ms: Option<u64>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1060 pub reviewer_id: Option<String>,
1061 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub approval_id: Option<String>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub reason: Option<String>,
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1069pub struct KnowledgePromotionResult {
1070 pub previous_status: KnowledgeItemStatus,
1071 pub previous_trust_level: KnowledgeTrustLevel,
1072 pub promoted: bool,
1073 pub item: KnowledgeItemRecord,
1074 pub coverage: KnowledgeCoverageRecord,
1075}
1076
1077#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1078#[serde(rename_all = "lowercase")]
1079pub enum NodeType {
1080 Directory,
1081 File,
1082}
1083
1084impl std::fmt::Display for NodeType {
1085 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086 match self {
1087 NodeType::Directory => write!(f, "directory"),
1088 NodeType::File => write!(f, "file"),
1089 }
1090 }
1091}
1092
1093impl std::str::FromStr for NodeType {
1094 type Err = String;
1095 fn from_str(s: &str) -> Result<Self, Self::Err> {
1096 match s.to_lowercase().as_str() {
1097 "directory" => Ok(NodeType::Directory),
1098 "file" => Ok(NodeType::File),
1099 _ => Err(format!("unknown node type: {}", s)),
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1105#[serde(rename_all = "UPPERCASE")]
1106pub enum LayerType {
1107 L0,
1108 L1,
1109 L2,
1110}
1111
1112impl std::fmt::Display for LayerType {
1113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1114 match self {
1115 LayerType::L0 => write!(f, "L0"),
1116 LayerType::L1 => write!(f, "L1"),
1117 LayerType::L2 => write!(f, "L2"),
1118 }
1119 }
1120}
1121
1122impl std::str::FromStr for LayerType {
1123 type Err = String;
1124 fn from_str(s: &str) -> Result<Self, Self::Err> {
1125 match s.to_uppercase().as_str() {
1126 "L0" | "L0_ABSTRACT" => Ok(LayerType::L0),
1127 "L1" | "L1_OVERVIEW" => Ok(LayerType::L1),
1128 "L2" | "L2_DETAIL" => Ok(LayerType::L2),
1129 _ => Err(format!("unknown layer type: {}", s)),
1130 }
1131 }
1132}
1133
1134impl LayerType {
1135 pub fn default_tokens(&self) -> usize {
1136 match self {
1137 LayerType::L0 => 100,
1138 LayerType::L1 => 2000,
1139 LayerType::L2 => 4000,
1140 }
1141 }
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1145pub struct RetrievalStep {
1146 pub step_type: String,
1147 pub description: String,
1148 pub layer_accessed: Option<LayerType>,
1149 pub nodes_evaluated: usize,
1150 pub scores: std::collections::HashMap<String, f64>,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1154pub struct NodeVisit {
1155 pub uri: String,
1156 pub node_type: NodeType,
1157 pub score: f64,
1158 pub depth: usize,
1159 pub layer_loaded: Option<LayerType>,
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1163pub struct RetrievalTrajectory {
1164 pub id: String,
1165 pub query: String,
1166 pub root_uri: String,
1167 pub steps: Vec<RetrievalStep>,
1168 pub visited_nodes: Vec<NodeVisit>,
1169 pub total_duration_ms: u64,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1173pub struct RetrievalResult {
1174 pub node_id: String,
1175 pub uri: String,
1176 pub content: String,
1177 pub layer_type: LayerType,
1178 pub score: f64,
1179 pub trajectory: RetrievalTrajectory,
1180}
1181
1182#[derive(Debug, Clone, Serialize, Deserialize)]
1183pub struct MemoryNode {
1184 pub id: String,
1185 pub uri: String,
1186 pub parent_uri: Option<String>,
1187 pub node_type: NodeType,
1188 pub created_at: DateTime<Utc>,
1189 pub updated_at: DateTime<Utc>,
1190 pub metadata: Option<serde_json::Value>,
1191}
1192
1193#[derive(Debug, Clone, Serialize, Deserialize)]
1194pub struct MemoryLayer {
1195 pub id: String,
1196 pub node_id: String,
1197 pub layer_type: LayerType,
1198 pub content: String,
1199 pub token_count: i64,
1200 pub embedding_id: Option<String>,
1201 pub created_at: DateTime<Utc>,
1202 pub source_chunk_id: Option<String>,
1203}
1204
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1206pub struct TreeNode {
1207 pub node: MemoryNode,
1208 pub children: Vec<TreeNode>,
1209 pub layer_summary: Option<LayerSummary>,
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize)]
1213pub struct LayerSummary {
1214 pub l0_preview: Option<String>,
1215 pub l1_preview: Option<String>,
1216 pub has_l2: bool,
1217}
1218
1219#[derive(Debug, Clone, Serialize, Deserialize)]
1220pub struct DirectoryListing {
1221 pub uri: String,
1222 pub nodes: Vec<MemoryNode>,
1223 pub total_children: usize,
1224 pub directories: Vec<MemoryNode>,
1225 pub files: Vec<MemoryNode>,
1226}
1227
1228#[derive(Debug, Clone, Serialize, Deserialize)]
1229pub struct DistilledFact {
1230 pub id: String,
1231 pub distillation_id: String,
1232 pub content: String,
1233 pub category: FactCategory,
1234 pub importance_score: f64,
1235 pub source_message_ids: Vec<String>,
1236 pub contradicts_fact_id: Option<String>,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1240#[serde(rename_all = "snake_case")]
1241pub enum FactCategory {
1242 UserPreference,
1243 TaskOutcome,
1244 Learning,
1245 Fact,
1246}
1247
1248impl std::fmt::Display for FactCategory {
1249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1250 match self {
1251 FactCategory::UserPreference => write!(f, "user_preference"),
1252 FactCategory::TaskOutcome => write!(f, "task_outcome"),
1253 FactCategory::Learning => write!(f, "learning"),
1254 FactCategory::Fact => write!(f, "fact"),
1255 }
1256 }
1257}
1258
1259#[derive(Debug, Clone, Serialize, Deserialize)]
1260pub struct DistillationReport {
1261 pub distillation_id: String,
1262 pub session_id: String,
1263 pub distilled_at: DateTime<Utc>,
1264 pub facts_extracted: usize,
1265 pub importance_threshold: f64,
1266 pub user_memory_updated: bool,
1267 pub agent_memory_updated: bool,
1268 #[serde(default)]
1269 pub stored_count: usize,
1270 #[serde(default)]
1271 pub deduped_count: usize,
1272 #[serde(default)]
1273 pub memory_ids: Vec<String>,
1274 #[serde(default)]
1275 pub candidate_ids: Vec<String>,
1276 #[serde(default)]
1277 pub status: String,
1278}
1279
1280#[derive(Debug, Clone, Serialize, Deserialize)]
1281pub struct SessionDistillation {
1282 pub id: String,
1283 pub session_id: String,
1284 pub distilled_at: DateTime<Utc>,
1285 pub input_token_count: i64,
1286 pub output_memory_count: usize,
1287 pub key_facts_extracted: usize,
1288 pub importance_threshold: f64,
1289}