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 let meta = record.meta.clone();
246 drop(guard);
249 self.hooks.notify_session_changed(&meta);
250 Ok(meta)
251 }
252
253 async fn list(&self, filter: ListFilter) -> StoreResult<Vec<SessionMeta>> {
254 let guard = lock(&self.inner);
255 let limit = filter.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
256 let mut out: Vec<SessionMeta> = guard
257 .sessions
258 .values()
259 .map(|record| record.meta.clone())
260 .filter(|meta| match_filter(meta, &filter))
261 .collect();
262 out.sort_by(|left, right| {
263 let timestamp_order = match filter.sort_by {
264 ListSortKey::CreatedAt => left.created_at_ms.cmp(&right.created_at_ms),
265 ListSortKey::UpdatedAt => left.updated_at_ms.cmp(&right.updated_at_ms),
266 };
267 let timestamp_order = match filter.order {
268 ListOrder::Ascending => timestamp_order,
269 ListOrder::Descending => timestamp_order.reverse(),
270 };
271 timestamp_order.then_with(|| left.id.cmp(&right.id))
272 });
273 if let Some(cursor) = filter.cursor.as_ref() {
274 let position = out.iter().position(|meta| meta.id == *cursor);
275 if let Some(start) = position {
276 out = out.into_iter().skip(start + 1).collect();
277 }
278 }
279 out.truncate(limit);
280 Ok(out)
281 }
282
283 async fn append(&self, session_id: &str, event: AppendEvent) -> StoreResult<StoredEvent> {
284 let mut guard = lock(&self.inner);
285 let record = guard
286 .sessions
287 .get_mut(session_id)
288 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
289 append_locked(record, &self.hooks, event)
290 }
291
292 async fn read(&self, session_id: &str, range: ReadRange) -> StoreResult<EventPage> {
293 let guard = lock(&self.inner);
294 let record = guard
295 .sessions
296 .get(session_id)
297 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
298 let from = range.from_event_id.unwrap_or(1);
299 let to = range.to_event_id.unwrap_or(EventId::MAX);
300 let limit = range.limit.unwrap_or(MAX_READ_BATCH).min(MAX_READ_BATCH);
301 let mut events: Vec<StoredEvent> = record
302 .events
303 .iter()
304 .filter(|event| event.event_id >= from && event.event_id <= to)
305 .take(limit)
306 .cloned()
307 .collect();
308 let next_cursor = if events.len() == limit {
309 events.last().map(|tail| tail.event_id + 1)
310 } else {
311 None
312 };
313 redact_stored_events(&self.hooks, &mut events)?;
315 Ok(EventPage {
316 events,
317 next_cursor,
318 })
319 }
320
321 async fn fork(
322 &self,
323 session_id: &str,
324 at_event_id: EventId,
325 child_id: Option<SessionId>,
326 ) -> StoreResult<ForkResult> {
327 let mut guard = lock(&self.inner);
328 let parent = guard
329 .sessions
330 .get(session_id)
331 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
332 if !parent
333 .events
334 .iter()
335 .any(|event| event.event_id == at_event_id)
336 {
337 return Err(StoreError::InvalidInput(format!(
338 "event {at_event_id} not found in session '{session_id}'"
339 )));
340 }
341 let new_id = child_id.unwrap_or_else(|| Uuid::now_v7().to_string());
342 if guard.sessions.contains_key(&new_id) {
343 return Err(StoreError::AlreadyExists(new_id));
344 }
345 let parent = guard.sessions.get(session_id).unwrap();
346 let (ms, text) = now_ms_and_rfc3339();
347 let mut child_meta = parent.meta.clone();
348 child_meta.id = new_id.clone();
349 child_meta.parent_session_id = Some(parent.meta.id.clone());
350 child_meta.created_at_ms = ms;
351 child_meta.created_at = text.clone();
352 child_meta.updated_at_ms = ms;
353 child_meta.updated_at = text;
354 child_meta.status = SessionStatus::Open;
355 child_meta.closed_at = None;
356 child_meta.closed_at_ms = None;
357 child_meta.soft_deleted_at_ms = None;
358 let mut parent_events: Vec<StoredEvent> = parent
359 .events
360 .iter()
361 .filter(|event| event.event_id <= at_event_id)
362 .cloned()
363 .collect();
364 prepare_stored_events_for_persistence(&self.hooks, &mut parent_events)?;
365 let copied_events = re_anchor_events(&parent_events, &new_id);
366 let copied_event_count = copied_events.len();
367 child_meta.event_count = copied_event_count;
368 child_meta.last_event_id = copied_events.last().map(|tail| tail.event_id);
369 child_meta.chain_root_hash = Some(chain_root_hash(&copied_events));
370 let next_event_id = copied_events
371 .last()
372 .map(|tail| tail.event_id + 1)
373 .unwrap_or(1);
374 let child_record = SessionRecord {
375 meta: child_meta,
376 events: copied_events,
377 next_event_id,
378 };
379 guard.sessions.insert(new_id.clone(), child_record);
380 Ok(ForkResult {
381 child_session_id: new_id,
382 forked_from_event_id: at_event_id,
383 copied_event_count,
384 })
385 }
386
387 async fn truncate(
388 &self,
389 session_id: &str,
390 at_event_id: EventId,
391 ) -> StoreResult<TruncateResult> {
392 let mut guard = lock(&self.inner);
393 let record = guard
394 .sessions
395 .get_mut(session_id)
396 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
397 if !record
398 .events
399 .iter()
400 .any(|event| event.event_id == at_event_id)
401 {
402 return Err(StoreError::InvalidInput(format!(
403 "event {at_event_id} not found in session '{session_id}'"
404 )));
405 }
406 let removed = record
407 .events
408 .iter()
409 .filter(|event| event.event_id > at_event_id)
410 .count();
411 record.events.retain(|event| event.event_id <= at_event_id);
412 record.next_event_id = at_event_id + 1;
413 record.meta.event_count = record.events.len();
414 record.meta.last_event_id = record.events.last().map(|tail| tail.event_id);
415 record.meta.chain_root_hash = Some(chain_root_hash(&record.events));
416 let (ms, text) = now_ms_and_rfc3339();
417 record.meta.updated_at_ms = ms;
418 record.meta.updated_at = text;
419 Ok(TruncateResult {
420 kept_event_count: record.events.len(),
421 removed_event_count: removed,
422 new_tip_event_id: record.events.last().map(|tail| tail.event_id),
423 })
424 }
425
426 async fn snapshot(&self, session_id: &str) -> StoreResult<Snapshot> {
427 let mut guard = lock(&self.inner);
428 let record = guard
429 .sessions
430 .get(session_id)
431 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
432 let (ms, text) = now_ms_and_rfc3339();
433 let mut events = record.events.clone();
434 redact_stored_events(&self.hooks, &mut events)?;
435 let snapshot = Snapshot {
436 id: SnapshotId(format!("snap-{}", Uuid::now_v7())),
437 session: record.meta.clone(),
438 events,
439 captured_at_ms: ms,
440 captured_at: text,
441 };
442 guard
443 .snapshots
444 .insert(snapshot.id.0.clone(), snapshot.clone());
445 Ok(snapshot)
446 }
447
448 async fn replay(&self, snapshot_id: &SnapshotId) -> StoreResult<Snapshot> {
449 let guard = lock(&self.inner);
450 let mut snapshot = guard
451 .snapshots
452 .get(&snapshot_id.0)
453 .cloned()
454 .ok_or_else(|| StoreError::NotFound(snapshot_id.0.clone()))?;
455 redact_stored_events(&self.hooks, &mut snapshot.events)?;
456 Ok(snapshot)
457 }
458
459 async fn close(&self, session_id: &str) -> StoreResult<StoredEvent> {
460 let mut guard = lock(&self.inner);
464 let record = guard
465 .sessions
466 .get_mut(session_id)
467 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
468 validate_open(&record.meta)?;
469 let record_root = record
470 .meta
471 .chain_root_hash
472 .clone()
473 .unwrap_or_else(|| chain_root_hash(&record.events));
474 let last_event_id = record.meta.last_event_id.unwrap_or(0);
475 let payload =
476 super::signing::canonical_receipt_payload(session_id, last_event_id, &record_root);
477 let mut append = AppendEvent::new(SessionEventKind::Receipt, payload);
478 append.actor = Some("session_store".into());
479 let mut stored = append_locked(record, &self.hooks, append)?;
480 if let Some(signer) = self
485 .hooks
486 .receipt_signer
487 .as_ref()
488 .or(self.hooks.event_signer.as_ref())
489 {
490 let signature = signer.sign_receipt(&record_root);
491 if let Some(receipt) = record
494 .events
495 .iter_mut()
496 .find(|event| event.event_id == stored.event_id)
497 {
498 receipt.signed_by = Some(signature.clone());
499 }
500 stored.signed_by = Some(signature);
501 }
502 let (ms, text) = now_ms_and_rfc3339();
503 record.meta.status = SessionStatus::Closed;
504 record.meta.closed_at_ms = Some(ms);
505 record.meta.closed_at = Some(text);
506 Ok(stored)
507 }
508
509 async fn soft_delete(&self, session_id: &str) -> StoreResult<SessionMeta> {
510 let mut guard = lock(&self.inner);
511 let record = guard
512 .sessions
513 .get_mut(session_id)
514 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
515 match record.meta.status {
516 SessionStatus::HardDeleted => return Err(StoreError::NotFound(session_id.to_string())),
517 SessionStatus::SoftDeleted => return Ok(record.meta.clone()),
518 _ => {}
519 }
520 let (ms, text) = now_ms_and_rfc3339();
521 record.meta.status = SessionStatus::SoftDeleted;
522 record.meta.soft_deleted_at_ms = Some(ms);
523 record.meta.updated_at_ms = ms;
524 record.meta.updated_at = text;
525 Ok(record.meta.clone())
526 }
527
528 async fn hard_delete(&self, session_id: &str) -> StoreResult<()> {
529 let mut guard = lock(&self.inner);
530 guard
531 .sessions
532 .remove(session_id)
533 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
534 Ok(())
535 }
536
537 async fn verify(&self, session_id: &str) -> StoreResult<VerifyReport> {
538 let guard = lock(&self.inner);
539 let record = guard
540 .sessions
541 .get(session_id)
542 .ok_or_else(|| StoreError::NotFound(session_id.to_string()))?;
543 let event_verifier = self
544 .hooks
545 .event_signer
546 .as_ref()
547 .map(|signer| signer.verifying_key());
548 let receipt_verifier = self
549 .hooks
550 .receipt_signer
551 .as_ref()
552 .or(self.hooks.event_signer.as_ref())
553 .map(|signer| signer.verifying_key());
554 Ok(verify_session_chain(
555 &record.meta,
556 &record.events,
557 event_verifier.as_ref(),
558 receipt_verifier.as_ref(),
559 ))
560 }
561
562 async fn search(&self, query: SearchQuery) -> StoreResult<SearchResponse> {
563 query.validate().map_err(StoreError::InvalidInput)?;
564 let embedder = self.hooks.embedder.clone();
565 let mut candidates = {
566 let guard = lock(&self.inner);
567 let mut candidates = Vec::new();
568 for record in guard.sessions.values() {
569 if !matches_search_filter(&record.meta, &query.filter) {
570 continue;
571 }
572 for event in &record.events {
573 candidates.push((record.meta.clone(), event.clone()));
574 }
575 }
576 candidates
577 };
578 let mut events = candidates
579 .iter()
580 .map(|(_, event)| event.clone())
581 .collect::<Vec<_>>();
582 redact_stored_events(&self.hooks, &mut events)?;
583 for ((_, candidate), redacted) in candidates.iter_mut().zip(events) {
584 *candidate = redacted;
585 }
586
587 let documents = candidates
588 .iter()
589 .map(|(meta, event)| {
590 redacted_search_document(self.hooks.redaction.as_ref(), meta, event)
591 })
592 .collect::<Vec<_>>();
593 let fts_scores = documents
594 .iter()
595 .map(|document| lexical_score(&query.query, document))
596 .collect::<Vec<_>>();
597 let semantic_available = embedder.is_semantic();
598 let semantic_scores = if semantic_available {
599 let query_vector = embedder.embed(&query.query);
600 embedder
601 .embed_batch(&documents)
602 .iter()
603 .map(|vector| super::search::cosine(&query_vector, vector).max(0.0))
604 .collect::<Vec<_>>()
605 } else {
606 vec![0.0; documents.len()]
607 };
608
609 let fts_ranks = ranks(&fts_scores);
610 let semantic_ranks = ranks(&semantic_scores);
611 let effective_mode = if semantic_available {
612 query.mode
613 } else {
614 SearchMode::Fts
615 };
616 let mut hits = candidates
617 .into_iter()
618 .enumerate()
619 .filter_map(|(index, (_, event))| {
620 let fts_rank = fts_ranks.get(&index).copied();
621 let semantic_rank = semantic_ranks.get(&index).copied();
622 let fts_score = (fts_scores[index] > 0.0).then_some(fts_scores[index]);
623 let semantic_score =
624 (semantic_scores[index] > 0.0).then_some(semantic_scores[index]);
625 let included = match effective_mode {
626 SearchMode::Fts => fts_rank.is_some(),
627 SearchMode::Semantic => semantic_rank.is_some(),
628 SearchMode::Hybrid => fts_rank.is_some() || semantic_rank.is_some(),
629 };
630 included.then(|| SearchHit {
631 session_id: event.session_id.clone(),
632 event_id: event.event_id,
633 kind: event.kind.clone(),
634 score: combined_score(
635 effective_mode,
636 fts_rank,
637 semantic_rank,
638 fts_score,
639 semantic_score,
640 ),
641 fts_score,
642 semantic_score,
643 snippet: snippet(&documents[index], &query.query, 240),
644 event,
645 })
646 })
647 .collect::<Vec<_>>();
648 hits.sort_by(|left, right| {
649 right
650 .score
651 .total_cmp(&left.score)
652 .then_with(|| left.session_id.cmp(&right.session_id))
653 .then_with(|| left.event_id.cmp(&right.event_id))
654 });
655 hits.truncate(query.limit());
656 let semantic_floor = !semantic_available;
657 Ok(SearchResponse {
658 requested_mode: query.mode,
659 effective_mode,
660 embedding_backend: embedder.name().to_string(),
661 semantic_floor,
662 fallback_reason: semantic_floor
663 .then(|| "semantic model unavailable; FTS-only fallback active".into()),
664 hits,
665 })
666 }
667}
668
669fn validate_parent(record: &SessionRecord, event: &AppendEvent) -> StoreResult<()> {
670 let Some(parent_event_id) = event.parent_event_id else {
671 return Ok(());
672 };
673 if record
674 .events
675 .iter()
676 .any(|stored| stored.event_id == parent_event_id)
677 {
678 Ok(())
679 } else {
680 Err(StoreError::InvalidInput(format!(
681 "parent_event_id {parent_event_id} not present in session"
682 )))
683 }
684}
685
686fn match_filter(meta: &SessionMeta, filter: &ListFilter) -> bool {
687 if let Some(tenant) = filter.tenant_id.as_ref() {
688 if meta.tenant_id.as_deref() != Some(tenant.as_str()) {
689 return false;
690 }
691 }
692 if let Some(persona) = filter.persona.as_ref() {
693 if meta.persona.as_deref() != Some(persona.as_str()) {
694 return false;
695 }
696 }
697 if let Some(status) = filter.status {
698 if meta.status != status {
699 return false;
700 }
701 }
702 if let Some(tag) = filter.tag.as_ref() {
703 if !meta.tags.iter().any(|value| value == tag) {
704 return false;
705 }
706 }
707 if let Some(parent_session_id) = filter.parent_session_id.as_ref() {
708 if meta.parent_session_id.as_ref() != Some(parent_session_id) {
709 return false;
710 }
711 }
712 if let Some(session_type) = filter.session_type {
713 if meta.session_type != Some(session_type) {
714 return false;
715 }
716 }
717 if let Some(project_scope) = filter.project_scope.as_ref() {
718 if meta.project_scope.as_ref() != Some(project_scope) {
719 return false;
720 }
721 }
722 if let Some(after) = filter.created_after_ms {
723 if meta.created_at_ms < after {
724 return false;
725 }
726 }
727 if let Some(before) = filter.created_before_ms {
728 if meta.created_at_ms > before {
729 return false;
730 }
731 }
732 true
733}
734
735fn matches_search_filter(meta: &SessionMeta, filter: &super::search::SearchFilter) -> bool {
736 if matches!(
737 meta.status,
738 SessionStatus::SoftDeleted | SessionStatus::HardDeleted
739 ) {
740 return false;
741 }
742 if let Some(tenant_id) = filter.tenant_id.as_ref() {
743 if meta.tenant_id.as_ref() != Some(tenant_id) {
744 return false;
745 }
746 }
747 if let Some(project_scope) = filter.project_scope.as_ref() {
748 if meta.project_scope.as_ref() != Some(project_scope) {
749 return false;
750 }
751 }
752 if let Some(session_id) = filter.session_id.as_ref() {
753 if &meta.id != session_id {
754 return false;
755 }
756 }
757 true
758}