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