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}