claude_agent/session/
mod.rs1pub 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 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}