fc2aac7c94
Gateway WebSocket chat sessions were in-memory only — conversation
history was lost on gateway restart, macOS sleep/wake, or client
reconnect. This wires up the existing SessionBackend (SQLite) to
the gateway WS handler so sessions survive restarts and reconnections.
Changes:
- Add delete_session() to SessionBackend trait + SQLite implementation
- Add session_persistence and session_ttl_hours to GatewayConfig
- Add Agent::seed_history() to hydrate agent from persisted messages
- Initialize SqliteSessionBackend in run_gateway() when enabled
- Send session_start message on WS connect with session_id + resumed
- Persist user/assistant messages after each turn
- Add GET /api/sessions and DELETE /api/sessions/{id} REST endpoints
- Bump version to 0.5.0
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.5 KiB
Rust
109 lines
3.5 KiB
Rust
//! 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,
|
|
/// 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,
|
|
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)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn session_metadata_is_constructible() {
|
|
let meta = SessionMetadata {
|
|
key: "test".into(),
|
|
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());
|
|
}
|
|
}
|