Skip to main content

locus_sdk/application/
memory_find.rs

1use 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    /// Create a deterministic memory finder over a shared node store.
22    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    /// Run predicate-based retrieval with stable sorting and pagination semantics.
38    ///
39    /// This operation does not apply resonance scoring; it filters, sorts,
40    /// and truncates nodes based only on explicit request criteria.
41    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}