Skip to main content

construct/channels/
session_backend.rs

1//! Trait abstraction for session persistence backends.
2//!
3//! Backends store per-sender conversation histories. The trait is intentionally
4//! minimal — load, append, remove_last, list — so that JSONL and SQLite (and
5//! future backends) share a common interface.
6
7use crate::providers::traits::ChatMessage;
8use chrono::{DateTime, Utc};
9
10/// Metadata about a persisted session.
11#[derive(Debug, Clone)]
12pub struct SessionMetadata {
13    /// Session key (e.g. `telegram_user123`).
14    pub key: String,
15    /// Optional human-readable name (e.g. `eyrie-commander-briefing`).
16    pub name: Option<String>,
17    /// When the session was first created.
18    pub created_at: DateTime<Utc>,
19    /// When the last message was appended.
20    pub last_activity: DateTime<Utc>,
21    /// Total number of messages in the session.
22    pub message_count: usize,
23}
24
25/// Query parameters for listing sessions.
26#[derive(Debug, Clone, Default)]
27pub struct SessionQuery {
28    /// Keyword to search in session messages (FTS5 if available).
29    pub keyword: Option<String>,
30    /// Maximum number of sessions to return.
31    pub limit: Option<usize>,
32}
33
34/// Trait for session persistence backends.
35///
36/// Implementations must be `Send + Sync` for sharing across async tasks.
37pub trait SessionBackend: Send + Sync {
38    /// Load all messages for a session. Returns empty vec if session doesn't exist.
39    fn load(&self, session_key: &str) -> Vec<ChatMessage>;
40
41    /// Append a single message to a session.
42    fn append(&self, session_key: &str, message: &ChatMessage) -> std::io::Result<()>;
43
44    /// Remove the last message from a session. Returns `true` if a message was removed.
45    fn remove_last(&self, session_key: &str) -> std::io::Result<bool>;
46
47    /// List all session keys.
48    fn list_sessions(&self) -> Vec<String>;
49
50    /// List sessions with metadata.
51    fn list_sessions_with_metadata(&self) -> Vec<SessionMetadata> {
52        // Default: construct metadata from messages (backends can override for efficiency)
53        self.list_sessions()
54            .into_iter()
55            .map(|key| {
56                let messages = self.load(&key);
57                SessionMetadata {
58                    key,
59                    name: None,
60                    created_at: Utc::now(),
61                    last_activity: Utc::now(),
62                    message_count: messages.len(),
63                }
64            })
65            .collect()
66    }
67
68    /// Compact a session file (remove duplicates/corruption). No-op by default.
69    fn compact(&self, _session_key: &str) -> std::io::Result<()> {
70        Ok(())
71    }
72
73    /// Remove sessions that haven't been active within the given TTL hours.
74    fn cleanup_stale(&self, _ttl_hours: u32) -> std::io::Result<usize> {
75        Ok(0)
76    }
77
78    /// Search sessions by keyword. Default returns empty (backends with FTS override).
79    fn search(&self, _query: &SessionQuery) -> Vec<SessionMetadata> {
80        Vec::new()
81    }
82
83    /// Delete all messages for a session. Returns `true` if the session existed.
84    fn delete_session(&self, _session_key: &str) -> std::io::Result<bool> {
85        Ok(false)
86    }
87
88    /// Set or update the human-readable name for a session.
89    fn set_session_name(&self, _session_key: &str, _name: &str) -> std::io::Result<()> {
90        Ok(())
91    }
92
93    /// Get the human-readable name for a session (if set).
94    fn get_session_name(&self, _session_key: &str) -> std::io::Result<Option<String>> {
95        Ok(None)
96    }
97
98    /// Set the session state (e.g. "idle", "running", "error").
99    /// `turn_id` identifies the current turn (set when running, cleared on idle).
100    fn set_session_state(
101        &self,
102        _session_key: &str,
103        _state: &str,
104        _turn_id: Option<&str>,
105    ) -> std::io::Result<()> {
106        Ok(())
107    }
108
109    /// Get the current session state. Returns `None` if the backend doesn't track state.
110    fn get_session_state(&self, _session_key: &str) -> std::io::Result<Option<SessionState>> {
111        Ok(None)
112    }
113
114    /// List sessions currently in "running" state.
115    fn list_running_sessions(&self) -> Vec<SessionMetadata> {
116        Vec::new()
117    }
118
119    /// List sessions stuck in "running" state longer than `threshold_secs`.
120    fn list_stuck_sessions(&self, _threshold_secs: u64) -> Vec<SessionMetadata> {
121        Vec::new()
122    }
123}
124
125/// Session state information.
126#[derive(Debug, Clone)]
127pub struct SessionState {
128    /// Current state: "idle", "running", or "error".
129    pub state: String,
130    /// Turn ID of the active or last turn.
131    pub turn_id: Option<String>,
132    /// When the current state was entered.
133    pub turn_started_at: Option<DateTime<Utc>>,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn session_metadata_is_constructible() {
142        let meta = SessionMetadata {
143            key: "test".into(),
144            name: None,
145            created_at: Utc::now(),
146            last_activity: Utc::now(),
147            message_count: 5,
148        };
149        assert_eq!(meta.key, "test");
150        assert_eq!(meta.message_count, 5);
151    }
152
153    #[test]
154    fn session_query_defaults() {
155        let q = SessionQuery::default();
156        assert!(q.keyword.is_none());
157        assert!(q.limit.is_none());
158    }
159}