locus_sdk/application/
memory_find.rs1use std::sync::Arc;
2
3use anyhow::Result;
4use locus_core_rs::domain::contracts::{NodeStore, SemanticIndexStore};
5use locus_core_rs::domain::models::{NodeQuery, SttpNode};
6use locus_core_rs::storage::derive_tenant_id_from_session;
7
8use crate::application::memory_filters::{
9 build_session_filter, node_matches_common_filters, resolve_indexed_sync_keys,
10};
11use crate::domain::memory::{
12 MemoryFindRequest, MemoryFindResult, MemorySortField, SortDirection, clamp_limit,
13};
14
15pub struct MemoryFindService {
16 store: Arc<dyn NodeStore>,
17 semantic_index: Option<Arc<dyn SemanticIndexStore>>,
18}
19
20impl MemoryFindService {
21 pub fn new(store: Arc<dyn NodeStore>) -> Self {
23 Self {
24 store,
25 semantic_index: None,
26 }
27 }
28
29 pub fn with_semantic_index(
30 mut self,
31 semantic_index: Arc<dyn SemanticIndexStore>,
32 ) -> Self {
33 self.semantic_index = Some(semantic_index);
34 self
35 }
36
37 pub async fn execute(&self, request: &MemoryFindRequest) -> Result<MemoryFindResult> {
42 let limit = clamp_limit(request.page.limit);
43 let query_limit = (limit.saturating_mul(5)).clamp(1, 5000);
44
45 let single_session = request
46 .scope
47 .session_ids
48 .as_deref()
49 .filter(|sessions| sessions.len() == 1)
50 .and_then(|sessions| sessions.first().cloned());
51
52 let mut nodes = self
53 .store
54 .query_nodes_async(NodeQuery {
55 limit: query_limit,
56 session_id: single_session.clone(),
57 from_utc: request.scope.from_utc,
58 to_utc: request.scope.to_utc,
59 tiers: request.scope.tiers.clone(),
60 })
61 .await?;
62
63 let session_filter = build_session_filter(&request.scope);
64 let tenant_id = request
65 .scope
66 .tenant_id
67 .clone()
68 .or_else(|| {
69 single_session
70 .as_deref()
71 .map(derive_tenant_id_from_session)
72 })
73 .unwrap_or_else(|| "default".to_string());
74
75 let indexed_sync_keys = if let Some(index) = self.semantic_index.as_ref() {
76 resolve_indexed_sync_keys(
77 index.as_ref(),
78 &tenant_id,
79 &request.filter,
80 single_session.as_deref(),
81 query_limit,
82 )
83 .await?
84 } else {
85 None
86 };
87
88 nodes.retain(|node| {
89 if let Some(keys) = &indexed_sync_keys
90 && !keys.contains(&node.sync_key)
91 {
92 return false;
93 }
94
95 node_matches_common_filters(node, &request.scope, &request.filter, session_filter.as_ref())
96 });
97 sort_nodes(&mut nodes, request.sort.field, request.sort.direction);
98
99 let has_more = nodes.len() > limit;
100 nodes.truncate(limit);
101
102 let next_cursor = nodes
103 .last()
104 .map(|node| format!("{}|{}", node.updated_at.to_rfc3339(), node.sync_key));
105
106 Ok(MemoryFindResult {
107 retrieved: nodes.len(),
108 nodes,
109 has_more,
110 next_cursor,
111 })
112 }
113}
114
115fn sort_nodes(nodes: &mut [SttpNode], field: MemorySortField, direction: SortDirection) {
116 use std::cmp::Ordering;
117
118 nodes.sort_by(|left, right| {
119 let ord = match field {
120 MemorySortField::Timestamp => left.timestamp.cmp(&right.timestamp),
121 MemorySortField::UpdatedAt => left.updated_at.cmp(&right.updated_at),
122 MemorySortField::Psi => left.psi.partial_cmp(&right.psi).unwrap_or(Ordering::Equal),
123 MemorySortField::Rho => left.rho.partial_cmp(&right.rho).unwrap_or(Ordering::Equal),
124 MemorySortField::Kappa => left
125 .kappa
126 .partial_cmp(&right.kappa)
127 .unwrap_or(Ordering::Equal),
128 };
129
130 match direction {
131 SortDirection::Asc => ord,
132 SortDirection::Desc => ord.reverse(),
133 }
134 });
135}