Skip to main content

claude_agent/session/
mod.rs

1//! Session management for stateful conversations.
2
3pub mod compact;
4pub mod manager;
5pub mod persistence;
6#[cfg(feature = "jsonl")]
7pub mod persistence_jsonl;
8#[cfg(feature = "postgres")]
9pub mod persistence_postgres;
10#[cfg(feature = "redis-backend")]
11pub mod persistence_redis;
12pub mod queue;
13pub mod session_state;
14pub mod state;
15pub mod types;
16
17pub use crate::types::TokenUsage;
18pub use compact::{CompactExecutor, CompactStrategy, DEFAULT_COMPACT_THRESHOLD};
19pub use manager::SessionManager;
20pub use persistence::{MemoryPersistence, Persistence, PersistenceFactory};
21#[cfg(feature = "jsonl")]
22pub use persistence_jsonl::{
23    JsonlConfig, JsonlConfigBuilder, JsonlEntry, JsonlPersistence, SyncMode,
24};
25#[cfg(feature = "postgres")]
26pub use persistence_postgres::{
27    PgPoolConfig, PostgresConfig, PostgresPersistence, PostgresSchema, SchemaIssue,
28};
29#[cfg(feature = "redis-backend")]
30pub use persistence_redis::{RedisConfig, RedisPersistence};
31pub use queue::{InputQueue, MergedInput, QueueError, QueuedInput, SharedInputQueue};
32pub use session_state::{ExecutionGuard, ToolState};
33pub use state::{
34    MessageId, MessageMetadata, Session, SessionConfig, SessionId, SessionMessage,
35    SessionPermissions, SessionState, SessionToolLimits, SessionType,
36};
37pub use types::{
38    CompactRecord, CompactTrigger, EnvironmentContext, Plan, PlanStatus, QueueItem, QueueOperation,
39    QueueStatus, SessionStats, SessionTree, SummarySnapshot, TodoItem, TodoStatus, ToolExecution,
40};
41
42use thiserror::Error;
43
44#[derive(Error, Debug)]
45pub enum SessionError {
46    #[error("Session not found: {id}")]
47    NotFound { id: String },
48
49    #[error("Session expired: {id}")]
50    Expired { id: String },
51
52    #[error("Storage error: {message}")]
53    Storage { message: String },
54
55    #[error("Serialization error: {0}")]
56    Serialization(#[from] serde_json::Error),
57
58    #[error("Compact error: {message}")]
59    Compact { message: String },
60
61    #[error("Context error: {0}")]
62    Context(#[from] crate::context::ContextError),
63}
64
65pub type SessionResult<T> = std::result::Result<T, SessionError>;
66
67#[cfg(any(feature = "postgres", feature = "redis-backend"))]
68pub(crate) trait StorageResultExt<T> {
69    fn storage_err(self) -> SessionResult<T>;
70    fn storage_err_ctx(self, context: &str) -> SessionResult<T>;
71}
72
73#[cfg(any(feature = "postgres", feature = "redis-backend"))]
74pub(crate) async fn with_retry<F, Fut, T>(
75    max_retries: u32,
76    initial_backoff: std::time::Duration,
77    max_backoff: std::time::Duration,
78    is_retryable: impl Fn(&SessionError) -> bool,
79    operation: F,
80) -> SessionResult<T>
81where
82    F: Fn() -> Fut,
83    Fut: std::future::Future<Output = SessionResult<T>>,
84{
85    let mut attempt = 0;
86    let mut backoff = initial_backoff;
87
88    loop {
89        match operation().await {
90            Ok(result) => return Ok(result),
91            Err(e) if attempt < max_retries && is_retryable(&e) => {
92                attempt += 1;
93                tracing::warn!(
94                    attempt = attempt,
95                    error = %e,
96                    "Retrying operation after transient failure"
97                );
98                // Symmetrical 10% jitter to prevent thundering herd
99                let jitter_factor = 1.0 + (rand::random::<f64>() * 0.2 - 0.1);
100                tokio::time::sleep(backoff.mul_f64(jitter_factor)).await;
101                backoff = (backoff * 2).min(max_backoff);
102            }
103            Err(e) => return Err(e),
104        }
105    }
106}
107
108#[cfg(any(feature = "postgres", feature = "redis-backend"))]
109impl<T, E: std::fmt::Display> StorageResultExt<T> for std::result::Result<T, E> {
110    fn storage_err(self) -> SessionResult<T> {
111        self.map_err(|e| SessionError::Storage {
112            message: e.to_string(),
113        })
114    }
115
116    fn storage_err_ctx(self, context: &str) -> SessionResult<T> {
117        self.map_err(|e| SessionError::Storage {
118            message: format!("{}: {}", context, e),
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_session_error_display() {
129        let err = SessionError::NotFound {
130            id: "test-123".to_string(),
131        };
132        assert!(err.to_string().contains("test-123"));
133    }
134
135    #[test]
136    fn test_session_error_expired() {
137        let err = SessionError::Expired {
138            id: "sess-456".to_string(),
139        };
140        assert!(err.to_string().contains("expired"));
141    }
142}