1use std::collections::BTreeMap;
7use std::sync::{Arc, Mutex};
8
9use async_trait::async_trait;
10use uuid::Uuid;
11
12use super::event::{now_ms_and_rfc3339, AppendEvent, EventId, SessionEventKind, StoredEvent};
13use super::memory_helpers::{meta_for_create, validate_open};
14use super::redaction::{
15 prepare_append_event, prepare_stored_events_for_persistence, redact_stored_events,
16};
17use super::search::{
18 combined_score, lexical_score, ranks, redacted_search_document, snippet, SearchHit, SearchMode,
19 SearchQuery, SearchResponse,
20};
21use super::signing::{
22 chain_root_fold, chain_root_hash, chain_root_init, compute_record_hash, re_anchor_events,
23 verify_session_chain,
24};
25use super::store::{
26 CreateSession, EventPage, ForkResult, ImportResult, ImportSession, ListFilter, ListOrder,
27 ListSortKey, ReadRange, SessionId, SessionImporter, SessionMeta, SessionStatus, SessionStore,
28 Snapshot, SnapshotId, StoreError, StoreHooks, StoreResult, TruncateResult, VerifyReport,
29 MAX_READ_BATCH,
30};
31
32struct SessionRecord {
33 meta: SessionMeta,
34 events: Vec<StoredEvent>,
35 next_event_id: EventId,
36}
37
38impl SessionRecord {
39 fn fresh(meta: SessionMeta) -> Self {
40 Self {
41 meta,
42 events: Vec::new(),
43 next_event_id: 1,
44 }
45 }
46}
47
48#[derive(Default)]
49struct Inner {
50 sessions: BTreeMap<SessionId, SessionRecord>,
51 snapshots: BTreeMap<String, Snapshot>,
52 imports: BTreeMap<String, ImportResult>,
53}
54
55#[derive(Clone)]
56pub struct MemorySessionStore {
57 inner: Arc<Mutex<Inner>>,
58 hooks: Arc<StoreHooks>,
59}
60
61impl MemorySessionStore {
62 pub fn new() -> Self {
63 Self::with_hooks(StoreHooks::default())
64 }
65
66 pub fn with_hooks(hooks: StoreHooks) -> Self {
67 Self {
68 inner: Arc::new(Mutex::new(Inner::default())),
69 hooks: Arc::new(hooks),
70 }
71 }
72}
73
74impl Default for MemorySessionStore {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80fn lock(inner: &Arc<Mutex<Inner>>) -> std::sync::MutexGuard<'_, Inner> {
81 inner.lock().unwrap_or_else(|e| e.into_inner())
82}
83
84fn append_locked(
90 record: &mut SessionRecord,
91 hooks: &StoreHooks,
92 mut event: AppendEvent,
93) -> StoreResult<StoredEvent> {
94 prepare_append_event(hooks, &mut event)?;
95 validate_open(&record.meta)?;
96 validate_parent(record, &event)?;
97 let (ts_ms, ts) = now_ms_and_rfc3339();
98 let event_id = record.next_event_id;
99 record.next_event_id = record.next_event_id.saturating_add(1);
100 let prev_hash = record.events.last().map(|tail| tail.record_hash.clone());
101 let mut stored = StoredEvent {
102 event_id,
103 session_id: record.meta.id.clone(),
104 tenant_id: record.meta.tenant_id.clone(),
105 parent_event_id: event.parent_event_id,
106 actor: event.actor,
107 kind: event.kind,
108 payload: event.payload,
109 tags: event.tags,
110 headers: event.headers,
111 ts_ms,
112 ts,
113 record_hash: String::new(),
114 prev_hash,
115 signed_by: None,
116 };
117 stored.record_hash = compute_record_hash(&stored);
118 if let Some(signer) = hooks.event_signer.as_ref() {
119 stored.signed_by = Some(signer.sign_event(&stored));
120 }
121 let prev_root = record
122 .meta
123 .chain_root_hash
124 .clone()
125 .unwrap_or_else(chain_root_init);
126 record.events.push(stored.clone());
127 record.meta.event_count = record.events.len();
128 record.meta.last_event_id = Some(event_id);
129 record.meta.updated_at_ms = ts_ms;
130 record.meta.updated_at = stored.ts.clone();
131 record.meta.chain_root_hash = Some(chain_root_fold(&prev_root, &stored.record_hash));
132 Ok(stored)
133}
134
135#[async_trait]
136impl SessionImporter for MemorySessionStore {
137 async fn import(&self, request: ImportSession) -> StoreResult<ImportResult> {
138 request.validate()?;
139 let mut guard = lock(&self.inner);
140 if let Some(existing) = guard.imports.get(&request.source_id) {
141 if existing.source_digest != request.source_digest {
142 return Err(StoreError::Conflict(format!(
143 "import source '{}' changed digest",
144 request.source_id
145 )));
146 }
147 let mut result = existing.clone();
148 result.imported = false;
149 return Ok(result);
150 }
151
152 let meta = meta_for_create(request.session);
153 if guard.sessions.contains_key(&meta.id) {
154 return Err(StoreError::AlreadyExists(meta.id));
155 }
156 let mut record = SessionRecord::fresh(meta.clone());
157 for event in request.events {
158 append_locked(&mut record, &self.hooks, event)?;
159 }
160 let result = ImportResult {
161 source_id: request.source_id.clone(),
162 source_digest: request.source_digest,
163 session_id: meta.id.clone(),
164 event_count: record.events.len(),
165 imported: true,
166 };
167 guard.sessions.insert(meta.id, record);
168 guard.imports.insert(request.source_id, result.clone());
169 Ok(result)
170 }
171}
172
173#[async_trait]
174impl SessionStore for MemorySessionStore {
175 fn hooks(&self) -> &StoreHooks {
176 &self.hooks
177 }
178
179 async fn create(&self, request: CreateSession) -> StoreResult<SessionMeta> {
180 let meta = meta_for_create(request);
181 let mut guard = lock(&self.inner);
182 if guard.sessions.contains_key(&meta.id) {
183 return Err(StoreError::AlreadyExists(meta.id));
184 }
185 guard
186 .sessions
187 .insert(meta.id.clone(), SessionRecord::fresh(meta.clone()));
188 Ok(meta)
189 }
190
191 async fn describe(&self, session_id: &str) -> StoreResult<SessionMeta> {
192 let guard = lock(&self.inner);
193 guard
194 .sessions
195 .get(session_id)
196 .map(|record| record.meta.clone())
197 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))
198 }
199
200 async fn update(
201 &self,
202 session_id: &str,
203 request: crate::UpdateSession,
204 ) -> StoreResult<SessionMeta> {
205 let mut guard = lock(&self.inner);
206 let record = guard
207 .sessions
208 .get_mut(session_id)
209 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
210 let (title, title_pinned) = crate::memory_helpers::resolve_title_update(
211 record.meta.title.take(),
212 record.meta.title_pinned,
213 request.title,
214 request.title_pinned,
215 );
216 record.meta.title = title;
217 record.meta.title_pinned = title_pinned;
218 if let Some(value) = request.cwd {
219 record.meta.cwd = Some(value);
220 }
221 if let Some(value) = request.model {
222 record.meta.model = Some(value);
223 }
224 if let Some(value) = request.parent_session_id {
225 record.meta.parent_session_id = Some(value);
226 }
227 if let Some(value) = request.session_type {
228 record.meta.session_type = Some(value);
229 }
230 if let Some(value) = request.project_scope {
231 record.meta.project_scope = Some(value);
232 }
233 if let Some(value) = request.usage_input {
234 record.meta.usage_input = value;
235 }
236 if let Some(value) = request.usage_output {
237 record.meta.usage_output = value;
238 }
239 if let Some(value) = request.usage_cost_usd_micros {
240 record.meta.usage_cost_usd_micros = value;
241 }
242 let (updated_at_ms, updated_at) = crate::event::now_ms_and_rfc3339();
243 record.meta.updated_at_ms = updated_at_ms;
244 record.meta.updated_at = updated_at;
245 Ok(record.meta.clone())
246 }
247
248 async fn list(&self, filter: ListFilter) -> StoreResult<Vec<SessionMeta>> {
249 let guard = lock(&self.inner);
250 let limit = filter.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
251 let mut out: Vec<SessionMeta> = guard
252 .sessions
253 .values()
254 .map(|record| record.meta.clone())
255 .filter(|meta| match_filter(meta, &filter))
256 .collect();
257 out.sort_by(|left, right| {
258 let timestamp_order = match filter.sort_by {
259 ListSortKey::CreatedAt => left.created_at_ms.cmp(&right.created_at_ms),
260 ListSortKey::UpdatedAt => left.updated_at_ms.cmp(&right.updated_at_ms),
261 };
262 let timestamp_order = match filter.order {
263 ListOrder::Ascending => timestamp_order,
264 ListOrder::Descending => timestamp_order.reverse(),
265 };
266 timestamp_order.then_with(|| left.id.cmp(&right.id))
267 });
268 if let Some(cursor) = filter.cursor.as_ref() {
269 let position = out.iter().position(|meta| meta.id == *cursor);
270 if let Some(start) = position {
271 out = out.into_iter().skip(start + 1).collect();
272 }
273 }
274 out.truncate(limit);
275 Ok(out)
276 }
277
278 async fn append(&self, session_id: &str, event: AppendEvent) -> StoreResult<StoredEvent> {
279 let mut guard = lock(&self.inner);
280 let record = guard
281 .sessions
282 .get_mut(session_id)
283 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
284 append_locked(record, &self.hooks, event)
285 }
286
287 async fn read(&self, session_id: &str, range: ReadRange) -> StoreResult<EventPage> {
288 let guard = lock(&self.inner);
289 let record = guard
290 .sessions
291 .get(session_id)
292 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
293 let from = range.from_event_id.unwrap_or(1);
294 let to = range.to_event_id.unwrap_or(EventId::MAX);
295 let limit = range.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
296 let mut events: Vec<StoredEvent> = record
297 .events
298 .iter()
299 .filter(|event| event.event_id >= from && event.event_id <= to)
300 .take(limit)
301 .cloned()
302 .collect();
303 let next_cursor = if events.len() == limit {
304 events.last().map(|tail| tail.event_id + 1)
305 } else {
306 None
307 };
308 redact_stored_events(&self.hooks, &mut events)?;
310 Ok(EventPage {
311 events,
312 next_cursor,
313 })
314 }
315
316 async fn fork(
317 &self,
318 session_id: &str,
319 at_event_id: EventId,
320 child_id: Option<SessionId>,
321 ) -> StoreResult<ForkResult> {
322 let mut guard = lock(&self.inner);
323 let parent = guard
324 .sessions
325 .get(session_id)
326 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
327 if !parent
328 .events
329 .iter()
330 .any(|event| event.event_id == at_event_id)
331 {
332 return Err(StoreError::InvalidInput(format!(
333 "event {at_event_id} not found in session '{session_id}'"
334 )));
335 }
336 let new_id = child_id.unwrap_or_else(|| Uuid::now_v7().to_string());
337 if guard.sessions.contains_key(&new_id) {
338 return Err(StoreError::AlreadyExists(new_id));
339 }
340 let parent = guard.sessions.get(session_id).unwrap();
341 let (ms, text) = now_ms_and_rfc3339();
342 let mut child_meta = parent.meta.clone();
343 child_meta.id = new_id.clone();
344 child_meta.parent_session_id = Some(parent.meta.id.clone());
345 child_meta.created_at_ms = ms;
346 child_meta.created_at = text.clone();
347 child_meta.updated_at_ms = ms;
348 child_meta.updated_at = text;
349 child_meta.status = SessionStatus::Open;
350 child_meta.closed_at = None;
351 child_meta.closed_at_ms = None;
352 child_meta.soft_deleted_at_ms = None;
353 let mut parent_events: Vec<StoredEvent> = parent
354 .events
355 .iter()
356 .filter(|event| event.event_id <= at_event_id)
357 .cloned()
358 .collect();
359 prepare_stored_events_for_persistence(&self.hooks, &mut parent_events)?;
360 let copied_events = re_anchor_events(&parent_events, &new_id);
361 let copied_event_count = copied_events.len();
362 child_meta.event_count = copied_event_count;
363 child_meta.last_event_id = copied_events.last().map(|tail| tail.event_id);
364 child_meta.chain_root_hash = Some(chain_root_hash(&copied_events));
365 let next_event_id = copied_events
366 .last()
367 .map(|tail| tail.event_id + 1)
368 .unwrap_or(1);
369 let child_record = SessionRecord {
370 meta: child_meta,
371 events: copied_events,
372 next_event_id,
373 };
374 guard.sessions.insert(new_id.clone(), child_record);
375 Ok(ForkResult {
376 child_session_id: new_id,
377 forked_from_event_id: at_event_id,
378 copied_event_count,
379 })
380 }
381
382 async fn truncate(
383 &self,
384 session_id: &str,
385 at_event_id: EventId,
386 ) -> StoreResult<TruncateResult> {
387 let mut guard = lock(&self.inner);
388 let record = guard
389 .sessions
390 .get_mut(session_id)
391 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
392 if !record
393 .events
394 .iter()
395 .any(|event| event.event_id == at_event_id)
396 {
397 return Err(StoreError::InvalidInput(format!(
398 "event {at_event_id} not found in session '{session_id}'"
399 )));
400 }
401 let removed = record
402 .events
403 .iter()
404 .filter(|event| event.event_id > at_event_id)
405 .count();
406 record.events.retain(|event| event.event_id <= at_event_id);
407 record.next_event_id = at_event_id + 1;
408 record.meta.event_count = record.events.len();
409 record.meta.last_event_id = record.events.last().map(|tail| tail.event_id);
410 record.meta.chain_root_hash = Some(chain_root_hash(&record.events));
411 let (ms, text) = now_ms_and_rfc3339();
412 record.meta.updated_at_ms = ms;
413 record.meta.updated_at = text;
414 Ok(TruncateResult {
415 kept_event_count: record.events.len(),
416 removed_event_count: removed,
417 new_tip_event_id: record.events.last().map(|tail| tail.event_id),
418 })
419 }
420
421 async fn snapshot(&self, session_id: &str) -> StoreResult<Snapshot> {
422 let mut guard = lock(&self.inner);
423 let record = guard
424 .sessions
425 .get(session_id)
426 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
427 let (ms, text) = now_ms_and_rfc3339();
428 let mut events = record.events.clone();
429 redact_stored_events(&self.hooks, &mut events)?;
430 let snapshot = Snapshot {
431 id: SnapshotId(format!("snap-{}", Uuid::now_v7())),
432 session: record.meta.clone(),
433 events,
434 captured_at_ms: ms,
435 captured_at: text,
436 };
437 guard
438 .snapshots
439 .insert(snapshot.id.0.clone(), snapshot.clone());
440 Ok(snapshot)
441 }
442
443 async fn replay(&self, snapshot_id: &SnapshotId) -> StoreResult<Snapshot> {
444 let guard = lock(&self.inner);
445 let mut snapshot = guard
446 .snapshots
447 .get(&snapshot_id.0)
448 .cloned()
449 .ok_or_else(|| StoreError::NotFound(snapshot_id.0.clone()))?;
450 redact_stored_events(&self.hooks, &mut snapshot.events)?;
451 Ok(snapshot)
452 }
453
454 async fn close(&self, session_id: &str) -> StoreResult<StoredEvent> {
455 let mut guard = lock(&self.inner);
459 let record = guard
460 .sessions
461 .get_mut(session_id)
462 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
463 validate_open(&record.meta)?;
464 let record_root = record
465 .meta
466 .chain_root_hash
467 .clone()
468 .unwrap_or_else(|| chain_root_hash(&record.events));
469 let last_event_id = record.meta.last_event_id.unwrap_or(0);
470 let payload =
471 super::signing::canonical_receipt_payload(session_id, last_event_id, &record_root);
472 let mut append = AppendEvent::new(SessionEventKind::Receipt, payload);
473 append.actor = Some("session_store".into());
474 let mut stored = append_locked(record, &self.hooks, append)?;
475 if let Some(signer) = self
480 .hooks
481 .receipt_signer
482 .as_ref()
483 .or(self.hooks.event_signer.as_ref())
484 {
485 let signature = signer.sign_receipt(&record_root);
486 if let Some(receipt) = record
489 .events
490 .iter_mut()
491 .find(|event| event.event_id == stored.event_id)
492 {
493 receipt.signed_by = Some(signature.clone());
494 }
495 stored.signed_by = Some(signature);
496 }
497 let (ms, text) = now_ms_and_rfc3339();
498 record.meta.status = SessionStatus::Closed;
499 record.meta.closed_at_ms = Some(ms);
500 record.meta.closed_at = Some(text);
501 Ok(stored)
502 }
503
504 async fn soft_delete(&self, session_id: &str) -> StoreResult<SessionMeta> {
505 let mut guard = lock(&self.inner);
506 let record = guard
507 .sessions
508 .get_mut(session_id)
509 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
510 match record.meta.status {
511 SessionStatus::HardDeleted => return Err(StoreError::NotFound(session_id.to_string())),
512 SessionStatus::SoftDeleted => return Ok(record.meta.clone()),
513 _ => {}
514 }
515 let (ms, text) = now_ms_and_rfc3339();
516 record.meta.status = SessionStatus::SoftDeleted;
517 record.meta.soft_deleted_at_ms = Some(ms);
518 record.meta.updated_at_ms = ms;
519 record.meta.updated_at = text;
520 Ok(record.meta.clone())
521 }
522
523 async fn hard_delete(&self, session_id: &str) -> StoreResult<()> {
524 let mut guard = lock(&self.inner);
525 guard
526 .sessions
527 .remove(session_id)
528 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
529 Ok(())
530 }
531
532 async fn verify(&self, session_id: &str) -> StoreResult<VerifyReport> {
533 let guard = lock(&self.inner);
534 let record = guard
535 .sessions
536 .get(session_id)
537 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
538 let event_verifier = self
539 .hooks
540 .event_signer
541 .as_ref()
542 .map(|signer| signer.verifying_key());
543 let receipt_verifier = self
544 .hooks
545 .receipt_signer
546 .as_ref()
547 .or(self.hooks.event_signer.as_ref())
548 .map(|signer| signer.verifying_key());
549 Ok(verify_session_chain(
550 &record.meta,
551 &record.events,
552 event_verifier.as_ref(),
553 receipt_verifier.as_ref(),
554 ))
555 }
556
557 async fn search(&self, query: SearchQuery) -> StoreResult<SearchResponse> {
558 query.validate().map_err(StoreError::InvalidInput)?;
559 let embedder = self.hooks.embedder.clone();
560 let mut candidates = {
561 let guard = lock(&self.inner);
562 let mut candidates = Vec::new();
563 for record in guard.sessions.values() {
564 if !matches_search_filter(&record.meta, &query.filter) {
565 continue;
566 }
567 for event in &record.events {
568 candidates.push((record.meta.clone(), event.clone()));
569 }
570 }
571 candidates
572 };
573 let mut events = candidates
574 .iter()
575 .map(|(_, event)| event.clone())
576 .collect::<Vec<_>>();
577 redact_stored_events(&self.hooks, &mut events)?;
578 for ((_, candidate), redacted) in candidates.iter_mut().zip(events) {
579 *candidate = redacted;
580 }
581
582 let documents = candidates
583 .iter()
584 .map(|(meta, event)| {
585 redacted_search_document(self.hooks.redaction.as_ref(), meta, event)
586 })
587 .collect::<Vec<_>>();
588 let fts_scores = documents
589 .iter()
590 .map(|document| lexical_score(&query.query, document))
591 .collect::<Vec<_>>();
592 let semantic_available = embedder.is_semantic();
593 let semantic_scores = if semantic_available {
594 let query_vector = embedder.embed(&query.query);
595 embedder
596 .embed_batch(&documents)
597 .iter()
598 .map(|vector| super::search::cosine(&query_vector, vector).max(0.0))
599 .collect::<Vec<_>>()
600 } else {
601 vec![0.0; documents.len()]
602 };
603
604 let fts_ranks = ranks(&fts_scores);
605 let semantic_ranks = ranks(&semantic_scores);
606 let effective_mode = if semantic_available {
607 query.mode
608 } else {
609 SearchMode::Fts
610 };
611 let mut hits = candidates
612 .into_iter()
613 .enumerate()
614 .filter_map(|(index, (_, event))| {
615 let fts_rank = fts_ranks.get(&index).copied();
616 let semantic_rank = semantic_ranks.get(&index).copied();
617 let fts_score = (fts_scores[index] > 0.0).then_some(fts_scores[index]);
618 let semantic_score =
619 (semantic_scores[index] > 0.0).then_some(semantic_scores[index]);
620 let included = match effective_mode {
621 SearchMode::Fts => fts_rank.is_some(),
622 SearchMode::Semantic => semantic_rank.is_some(),
623 SearchMode::Hybrid => fts_rank.is_some() || semantic_rank.is_some(),
624 };
625 included.then(|| SearchHit {
626 session_id: event.session_id.clone(),
627 event_id: event.event_id,
628 kind: event.kind.clone(),
629 score: combined_score(
630 effective_mode,
631 fts_rank,
632 semantic_rank,
633 fts_score,
634 semantic_score,
635 ),
636 fts_score,
637 semantic_score,
638 snippet: snippet(&documents[index], &query.query, 240),
639 event,
640 })
641 })
642 .collect::<Vec<_>>();
643 hits.sort_by(|left, right| {
644 right
645 .score
646 .total_cmp(&left.score)
647 .then_with(|| left.session_id.cmp(&right.session_id))
648 .then_with(|| left.event_id.cmp(&right.event_id))
649 });
650 hits.truncate(query.limit());
651 let semantic_floor = !semantic_available;
652 Ok(SearchResponse {
653 requested_mode: query.mode,
654 effective_mode,
655 embedding_backend: embedder.name().to_string(),
656 semantic_floor,
657 fallback_reason: semantic_floor
658 .then(|| "semantic model unavailable; FTS-only fallback active".into()),
659 hits,
660 })
661 }
662}
663
664fn validate_parent(record: &SessionRecord, event: &AppendEvent) -> StoreResult<()> {
665 let Some(parent_event_id) = event.parent_event_id else {
666 return Ok(());
667 };
668 if record
669 .events
670 .iter()
671 .any(|stored| stored.event_id == parent_event_id)
672 {
673 Ok(())
674 } else {
675 Err(StoreError::InvalidInput(format!(
676 "parent_event_id {parent_event_id} not present in session"
677 )))
678 }
679}
680
681fn match_filter(meta: &SessionMeta, filter: &ListFilter) -> bool {
682 if let Some(tenant) = filter.tenant_id.as_ref() {
683 if meta.tenant_id.as_deref() != Some(tenant.as_str()) {
684 return false;
685 }
686 }
687 if let Some(persona) = filter.persona.as_ref() {
688 if meta.persona.as_deref() != Some(persona.as_str()) {
689 return false;
690 }
691 }
692 if let Some(status) = filter.status {
693 if meta.status != status {
694 return false;
695 }
696 }
697 if let Some(tag) = filter.tag.as_ref() {
698 if !meta.tags.iter().any(|value| value == tag) {
699 return false;
700 }
701 }
702 if let Some(parent_session_id) = filter.parent_session_id.as_ref() {
703 if meta.parent_session_id.as_ref() != Some(parent_session_id) {
704 return false;
705 }
706 }
707 if let Some(session_type) = filter.session_type {
708 if meta.session_type != Some(session_type) {
709 return false;
710 }
711 }
712 if let Some(project_scope) = filter.project_scope.as_ref() {
713 if meta.project_scope.as_ref() != Some(project_scope) {
714 return false;
715 }
716 }
717 if let Some(after) = filter.created_after_ms {
718 if meta.created_at_ms < after {
719 return false;
720 }
721 }
722 if let Some(before) = filter.created_before_ms {
723 if meta.created_at_ms > before {
724 return false;
725 }
726 }
727 true
728}
729
730fn matches_search_filter(meta: &SessionMeta, filter: &super::search::SearchFilter) -> bool {
731 if matches!(
732 meta.status,
733 SessionStatus::SoftDeleted | SessionStatus::HardDeleted
734 ) {
735 return false;
736 }
737 if let Some(tenant_id) = filter.tenant_id.as_ref() {
738 if meta.tenant_id.as_ref() != Some(tenant_id) {
739 return false;
740 }
741 }
742 if let Some(project_scope) = filter.project_scope.as_ref() {
743 if meta.project_scope.as_ref() != Some(project_scope) {
744 return false;
745 }
746 }
747 if let Some(session_id) = filter.session_id.as_ref() {
748 if &meta.id != session_id {
749 return false;
750 }
751 }
752 true
753}