Add is_online/is_persistent for persistent agent sessions

- Sessions now track whether agent is online or offline
- Persistent agents (no support code) stay in session list when disconnected
- Dashboard shows online/offline status with color indicator
- Connect/Chat buttons disabled when agent is offline
- Agent reconnection reuses existing session
This commit is contained in:
2025-12-28 17:52:26 -07:00
parent 3c2e0708ef
commit 1cc94c61e7
4 changed files with 81 additions and 8 deletions

View File

@@ -18,6 +18,8 @@ pub struct SessionInfo {
pub started_at: String,
pub viewer_count: usize,
pub is_streaming: bool,
pub is_online: bool,
pub is_persistent: bool,
pub last_heartbeat: String,
pub os_version: Option<String>,
pub is_elevated: bool,
@@ -34,6 +36,8 @@ impl From<crate::session::Session> for SessionInfo {
started_at: s.started_at.to_rfc3339(),
viewer_count: s.viewer_count,
is_streaming: s.is_streaming,
is_online: s.is_online,
is_persistent: s.is_persistent,
last_heartbeat: s.last_heartbeat.to_rfc3339(),
os_version: s.os_version,
is_elevated: s.is_elevated,

View File

@@ -97,7 +97,9 @@ async fn handle_agent_connection(
}
// Register the agent and get channels
let (session_id, frame_tx, mut input_rx) = sessions.register_agent(agent_id.clone(), agent_name.clone()).await;
// Persistent agents (no support code) keep their session when disconnected
let is_persistent = support_code.is_none();
let (session_id, frame_tx, mut input_rx) = sessions.register_agent(agent_id.clone(), agent_name.clone(), is_persistent).await;
info!("Session created: {} (agent in idle mode)", session_id);
@@ -221,7 +223,8 @@ async fn handle_agent_connection(
// Cleanup
input_forward.abort();
cancel_check.abort();
sessions_cleanup.remove_session(session_id).await;
// Mark agent as disconnected (persistent agents stay in list as offline)
sessions_cleanup.mark_agent_disconnected(session_id).await;
// Mark support code as completed if one was used (unless cancelled)
if let Some(ref code) = support_code_cleanup {

View File

@@ -30,6 +30,8 @@ pub struct Session {
pub started_at: chrono::DateTime<chrono::Utc>,
pub viewer_count: usize,
pub is_streaming: bool,
pub is_online: bool, // Whether agent is currently connected
pub is_persistent: bool, // Persistent agent (no support code) vs support session
pub last_heartbeat: chrono::DateTime<chrono::Utc>,
// Agent status info
pub os_version: Option<String>,
@@ -76,7 +78,35 @@ impl SessionManager {
}
/// Register a new agent and create a session
pub async fn register_agent(&self, agent_id: AgentId, agent_name: String) -> (SessionId, FrameSender, InputReceiver) {
/// If agent was previously connected (offline session exists), reuse that session
pub async fn register_agent(&self, agent_id: AgentId, agent_name: String, is_persistent: bool) -> (SessionId, FrameSender, InputReceiver) {
// Check if this agent already has an offline session (reconnecting)
{
let agents = self.agents.read().await;
if let Some(&existing_session_id) = agents.get(&agent_id) {
let mut sessions = self.sessions.write().await;
if let Some(session_data) = sessions.get_mut(&existing_session_id) {
if !session_data.info.is_online {
// Reuse existing session - mark as online and create new channels
tracing::info!("Agent {} reconnecting to existing session {}", agent_id, existing_session_id);
let (frame_tx, _) = broadcast::channel(16);
let (input_tx, input_rx) = tokio::sync::mpsc::channel(64);
session_data.info.is_online = true;
session_data.info.last_heartbeat = chrono::Utc::now();
session_data.info.agent_name = agent_name; // Update name in case it changed
session_data.frame_tx = frame_tx.clone();
session_data.input_tx = input_tx;
session_data.last_heartbeat_instant = Instant::now();
return (existing_session_id, frame_tx, input_rx);
}
}
}
}
// Create new session
let session_id = Uuid::new_v4();
// Create channels
@@ -91,6 +121,8 @@ impl SessionManager {
started_at: now,
viewer_count: 0,
is_streaming: false,
is_online: true,
is_persistent,
last_heartbeat: now,
os_version: None,
is_elevated: false,
@@ -255,10 +287,37 @@ impl SessionManager {
}
}
/// Remove a session (when agent disconnects)
/// Mark agent as disconnected
/// For persistent agents: keep session but mark as offline
/// For support sessions: remove session entirely
pub async fn mark_agent_disconnected(&self, session_id: SessionId) {
let mut sessions = self.sessions.write().await;
if let Some(session_data) = sessions.get_mut(&session_id) {
if session_data.info.is_persistent {
// Persistent agent - keep session but mark as offline
tracing::info!("Persistent agent {} marked offline (session {} preserved)",
session_data.info.agent_id, session_id);
session_data.info.is_online = false;
session_data.info.is_streaming = false;
session_data.info.viewer_count = 0;
session_data.viewers.clear();
} else {
// Support session - remove entirely
let agent_id = session_data.info.agent_id.clone();
sessions.remove(&session_id);
drop(sessions); // Release sessions lock before acquiring agents lock
let mut agents = self.agents.write().await;
agents.remove(&agent_id);
tracing::info!("Support session {} removed", session_id);
}
}
}
/// Remove a session entirely (for cleanup)
pub async fn remove_session(&self, session_id: SessionId) {
let mut sessions = self.sessions.write().await;
if let Some(session_data) = sessions.remove(&session_id) {
drop(sessions);
let mut agents = self.agents.write().await;
agents.remove(&session_data.info.agent_id);
}