Skip to main content

opendev_runtime/
session_status.rs

1//! Session status tracking for monitoring agent activity.
2//!
3//! Tracks per-session status (idle, busy, retry) and publishes changes
4//! via the event bus. The TUI uses this to display retry countdowns
5//! and activity indicators.
6
7use std::collections::HashMap;
8use std::sync::Mutex;
9
10use serde::{Deserialize, Serialize};
11
12use crate::event_bus::{EventBus, RuntimeEvent, now_ms};
13
14/// The current status of a session.
15#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
16#[serde(tag = "type")]
17pub enum SessionStatus {
18    /// Session is idle, waiting for user input.
19    #[default]
20    #[serde(rename = "idle")]
21    Idle,
22    /// Session is actively processing a request.
23    #[serde(rename = "busy")]
24    Busy,
25    /// Session is waiting to retry after an error.
26    #[serde(rename = "retry")]
27    Retry {
28        /// Which retry attempt this is (1-based).
29        attempt: u32,
30        /// Human-readable reason for the retry (e.g. "Rate Limited").
31        message: String,
32        /// Unix timestamp in milliseconds when the next retry will occur.
33        next_retry_ms: u64,
34    },
35}
36
37impl std::fmt::Display for SessionStatus {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Idle => write!(f, "idle"),
41            Self::Busy => write!(f, "busy"),
42            Self::Retry {
43                attempt, message, ..
44            } => write!(f, "retry (attempt {attempt}: {message})"),
45        }
46    }
47}
48
49/// Tracks the status of all active sessions.
50///
51/// Thread-safe via interior `Mutex`. Status changes are published
52/// to an optional [`EventBus`].
53pub struct SessionStatusTracker {
54    state: Mutex<HashMap<String, SessionStatus>>,
55    event_bus: Option<EventBus>,
56}
57
58impl SessionStatusTracker {
59    /// Create a new tracker without event publishing.
60    pub fn new() -> Self {
61        Self {
62            state: Mutex::new(HashMap::new()),
63            event_bus: None,
64        }
65    }
66
67    /// Create a new tracker that publishes status changes to the event bus.
68    pub fn with_event_bus(event_bus: EventBus) -> Self {
69        Self {
70            state: Mutex::new(HashMap::new()),
71            event_bus: Some(event_bus),
72        }
73    }
74
75    /// Get the current status of a session.
76    ///
77    /// Returns `SessionStatus::Idle` if the session has no tracked status.
78    pub fn get(&self, session_id: &str) -> SessionStatus {
79        self.state
80            .lock()
81            .expect("SessionStatusTracker lock poisoned")
82            .get(session_id)
83            .cloned()
84            .unwrap_or_default()
85    }
86
87    /// Set the status of a session.
88    ///
89    /// Setting `Idle` removes the session from tracking (it's the default).
90    /// Publishes a `SessionStatusChanged` event if an event bus is configured.
91    pub fn set(&self, session_id: impl Into<String>, status: SessionStatus) {
92        let session_id = session_id.into();
93        let mut state = self
94            .state
95            .lock()
96            .expect("SessionStatusTracker lock poisoned");
97
98        match &status {
99            SessionStatus::Idle => {
100                state.remove(&session_id);
101            }
102            _ => {
103                state.insert(session_id.clone(), status.clone());
104            }
105        }
106
107        // Publish event
108        if let Some(bus) = &self.event_bus {
109            bus.publish(RuntimeEvent::SessionStatusChanged {
110                session_id,
111                status,
112                timestamp_ms: now_ms(),
113            });
114        }
115    }
116
117    /// Mark a session as busy.
118    pub fn set_busy(&self, session_id: impl Into<String>) {
119        self.set(session_id, SessionStatus::Busy);
120    }
121
122    /// Mark a session as idle.
123    pub fn set_idle(&self, session_id: impl Into<String>) {
124        self.set(session_id, SessionStatus::Idle);
125    }
126
127    /// Mark a session as retrying.
128    pub fn set_retry(
129        &self,
130        session_id: impl Into<String>,
131        attempt: u32,
132        message: impl Into<String>,
133        next_retry_ms: u64,
134    ) {
135        self.set(
136            session_id,
137            SessionStatus::Retry {
138                attempt,
139                message: message.into(),
140                next_retry_ms,
141            },
142        );
143    }
144
145    /// Get all tracked sessions and their statuses.
146    pub fn list(&self) -> HashMap<String, SessionStatus> {
147        self.state
148            .lock()
149            .expect("SessionStatusTracker lock poisoned")
150            .clone()
151    }
152
153    /// Get the number of tracked (non-idle) sessions.
154    pub fn active_count(&self) -> usize {
155        self.state
156            .lock()
157            .expect("SessionStatusTracker lock poisoned")
158            .len()
159    }
160
161    /// Check if any session is currently retrying.
162    pub fn has_retrying(&self) -> bool {
163        self.state
164            .lock()
165            .expect("SessionStatusTracker lock poisoned")
166            .values()
167            .any(|s| matches!(s, SessionStatus::Retry { .. }))
168    }
169}
170
171impl Default for SessionStatusTracker {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl std::fmt::Debug for SessionStatusTracker {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let count = self.active_count();
180        f.debug_struct("SessionStatusTracker")
181            .field("active_sessions", &count)
182            .finish()
183    }
184}
185
186#[cfg(test)]
187#[path = "session_status_tests.rs"]
188mod tests;