C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs:162:    pub async fn update_agent_status(
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-163-        &self,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-164-        session_id: SessionId,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-165-        os_version: Option<String>,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-166-        is_elevated: bool,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-167-        uptime_secs: i64,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-168-        display_count: i32,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-169-        is_streaming: bool,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-170-    ) {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-171-        let mut sessions = self.sessions.write().await;
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-172-        if let Some(session_data) = sessions.get_mut(&session_id) {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-173-            session_data.info.last_heartbeat = chrono::Utc::now();
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-174-            session_data.last_heartbeat_instant = Instant::now();
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-175-            session_data.info.is_streaming = is_streaming;
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-176-            if let Some(os) = os_version {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-177-                session_data.info.os_version = Some(os);


I need to update the `update_agent_status` function to also accept and track the agent_version. Let me read the full session module to understand the Session struct and then make the necessary updates.


     1→//! Session management for GuruConnect
     2→//!
     3→//! Manages active remote desktop sessions, tracking which agents
     4→//! are connected and which viewers are watching them.
     5→
     6→use std::collections::HashMap;
     7→use std::sync::Arc;
     8→use std::time::Instant;
     9→use tokio::sync::{broadcast, RwLock};
    10→use uuid::Uuid;
    11→
    12→/// Unique identifier for a session
    13→pub type SessionId = Uuid;
    14→
    15→/// Unique identifier for an agent
    16→pub type AgentId = String;
    17→
    18→/// Unique identifier for a viewer
    19→pub type ViewerId = String;
    20→
    21→/// Information about a connected viewer/technician
    22→#[derive(Debug, Clone)]
    23→pub struct ViewerInfo {
    24→    pub id: ViewerId,
    25→    pub name: String,
    26→    pub connected_at: chrono::DateTime<chrono::Utc>,
    27→}
    28→
    29→/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
    30→const HEARTBEAT_TIMEOUT_SECS: u64 = 90;
    31→
    32→/// Session state
    33→#[derive(Debug, Clone)]
    34→pub struct Session {
    35→    pub id: SessionId,
    36→    pub agent_id: AgentId,
    37→    pub agent_name: String,
    38→    pub started_at: chrono::DateTime<chrono::Utc>,
    39→    pub viewer_count: usize,
    40→    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    41→    pub is_streaming: bool,
    42→    pub is_online: bool,  // Whether agent is currently connected
    43→    pub is_persistent: bool,  // Persistent agent (no support code) vs support session
    44→    pub last_heartbeat: chrono::DateTime<chrono::Utc>,
    45→    // Agent status info
    46→    pub os_version: Option<String>,
    47→    pub is_elevated: bool,
    48→    pub uptime_secs: i64,
    49→    pub display_count: i32,
    50→}
    51→
    52→/// Channel for sending frames from agent to viewers
    53→pub type FrameSender = broadcast::Sender<Vec<u8>>;
    54→pub type FrameReceiver = broadcast::Receiver<Vec<u8>>;
    55→
    56→/// Channel for sending input events from viewer to agent
    57→pub type InputSender = tokio::sync::mpsc::Sender<Vec<u8>>;
    58→pub type InputReceiver = tokio::sync::mpsc::Receiver<Vec<u8>>;
    59→
    60→/// Internal session data with channels
    61→struct SessionData {
    62→    info: Session,
    63→    /// Channel for video frames (agent -> viewers)
    64→    frame_tx: FrameSender,
    65→    /// Channel for input events (viewer -> agent)
    66→    input_tx: InputSender,
    67→    input_rx: Option<InputReceiver>,
    68→    /// Map of connected viewers (id -> info)
    69→    viewers: HashMap<ViewerId, ViewerInfo>,
    70→    /// Instant for heartbeat tracking
    71→    last_heartbeat_instant: Instant,
    72→}
    73→
    74→/// Manages all active sessions
    75→#[derive(Clone)]
    76→pub struct SessionManager {
    77→    sessions: Arc<RwLock<HashMap<SessionId, SessionData>>>,
    78→    agents: Arc<RwLock<HashMap<AgentId, SessionId>>>,
    79→}
    80→
    81→impl SessionManager {
    82→    pub fn new() -> Self {
    83→        Self {
    84→            sessions: Arc::new(RwLock::new(HashMap::new())),
    85→            agents: Arc::new(RwLock::new(HashMap::new())),
    86→        }
    87→    }
    88→
    89→    /// Register a new agent and create a session
    90→    /// If agent was previously connected (offline session exists), reuse that session
    91→    pub async fn register_agent(&self, agent_id: AgentId, agent_name: String, is_persistent: bool) -> (SessionId, FrameSender, InputReceiver) {
    92→        // Check if this agent already has an offline session (reconnecting)
    93→        {
    94→            let agents = self.agents.read().await;
    95→            if let Some(&existing_session_id) = agents.get(&agent_id) {
    96→                let mut sessions = self.sessions.write().await;
    97→                if let Some(session_data) = sessions.get_mut(&existing_session_id) {
    98→                    if !session_data.info.is_online {
    99→                        // Reuse existing session - mark as online and create new channels
   100→                        tracing::info!("Agent {} reconnecting to existing session {}", agent_id, existing_session_id);

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Session management for GuruConnect
//!
//! Manages active remote desktop sessions, tracking which agents
//! are connected and which viewers are watching them.

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;

/// Unique identifier for a session
pub type SessionId = Uuid;

/// Unique identifier for an agent
pub type AgentId = String;

/// Unique identifier for a viewer
pub type ViewerId = String;

/// Information about a connected viewer/technician
#[derive(Debug, Clone)]
pub struct ViewerInfo {
    pub id: ViewerId,
    pub name: String,
    pub connected_at: chrono::DateTime<chrono::Utc>,
}

/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
const HEARTBEAT_TIMEOUT_SECS: u64 = 90;

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
}

/// Channel for sending frames from agent to viewers
pub type FrameSender = broadcast::Sender<Vec<u8>>;
pub type FrameReceiver = broadcast::Receiver<Vec<u8>>;

/// Channel for sending input events from viewer to agent
pub type InputSender = tokio::sync::mpsc::Sender<Vec<u8>>;
pub type InputReceiver = tokio::sync::mpsc::Receiver<Vec<u8>>;

/// Internal session data with channels
struct SessionData {
    info: Session,
    /// Channel for video frames (agent -> viewers)
    frame_tx: FrameSender,
    /// Channel for input events (viewer -> agent)
    input_tx: InputSender,
    input_rx: Option<InputReceiver>,
    /// Map of connected viewers (id -> info)
    viewers: HashMap<ViewerId, ViewerInfo>,
    /// Instant for heartbeat tracking
    last_heartbeat_instant: Instant,
}

/// Manages all active sessions
#[derive(Clone)]
pub struct SessionManager {
    sessions: Arc<RwLock<HashMap<SessionId, SessionData>>>,
    agents: Arc<RwLock<HashMap<AgentId, SessionId>>>,
}

impl SessionManager {
    pub fn new() -> Self {
        Self {
            sessions: Arc::new(RwLock::new(HashMap::new())),
            agents: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Register a new agent and create a session
    /// 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);


I need to add `agent_version` to the Session struct. Let me also read the update_agent_status function fully, then make the necessary changes.


   160→
   161→    /// Update agent status from heartbeat or status message
   162→    pub async fn update_agent_status(
   163→        &self,
   164→        session_id: SessionId,
   165→        os_version: Option<String>,
   166→        is_elevated: bool,
   167→        uptime_secs: i64,
   168→        display_count: i32,
   169→        is_streaming: bool,
   170→    ) {
   171→        let mut sessions = self.sessions.write().await;
   172→        if let Some(session_data) = sessions.get_mut(&session_id) {
   173→            session_data.info.last_heartbeat = chrono::Utc::now();
   174→            session_data.last_heartbeat_instant = Instant::now();
   175→            session_data.info.is_streaming = is_streaming;
   176→            if let Some(os) = os_version {
   177→                session_data.info.os_version = Some(os);
   178→            }
   179→            session_data.info.is_elevated = is_elevated;
   180→            session_data.info.uptime_secs = uptime_secs;
   181→            session_data.info.display_count = display_count;
   182→        }
   183→    }
   184→
   185→    /// Update heartbeat timestamp
   186→    pub async fn update_heartbeat(&self, session_id: SessionId) {
   187→        let mut sessions = self.sessions.write().await;
   188→        if let Some(session_data) = sessions.get_mut(&session_id) {
   189→            session_data.info.last_heartbeat = chrono::Utc::now();
   190→            session_data.last_heartbeat_instant = Instant::now();
   191→        }
   192→    }
   193→
   194→    /// Check if a session has timed out (no heartbeat for too long)
   195→    pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool {
   196→        let sessions = self.sessions.read().await;
   197→        if let Some(session_data) = sessions.get(&session_id) {
   198→            session_data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS
   199→        } else {

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
        }
    }

    /// Update heartbeat timestamp
    pub async fn update_heartbeat(&self, session_id: SessionId) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
        }
    }

    /// Check if a session has timed out (no heartbeat for too long)
    pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            session_data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS
        } else {


Now I need to:
1. Add `agent_version` field to the Session struct
2. Update the `update_agent_status` method to accept and store agent_version

Let me make these changes.


Now I'll update the Session struct to include agent_version:


Edit

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
}

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
    pub agent_version: Option<String>,  // Agent software version
}


The file C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
    28→
    29→/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
    30→const HEARTBEAT_TIMEOUT_SECS: u64 = 90;
    31→
    32→/// Session state
    33→#[derive(Debug, Clone)]
    34→pub struct Session {
    35→    pub id: SessionId,
    36→    pub agent_id: AgentId,
    37→    pub agent_name: String,
    38→    pub started_at: chrono::DateTime<chrono::Utc>,
    39→    pub viewer_count: usize,
    40→    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    41→    pub is_streaming: bool,
    42→    pub is_online: bool,  // Whether agent is currently connected
    43→    pub is_persistent: bool,  // Persistent agent (no support code) vs support session
    44→    pub last_heartbeat: chrono::DateTime<chrono::Utc>,
    45→    // Agent status info
    46→    pub os_version: Option<String>,
    47→    pub is_elevated: bool,
    48→    pub uptime_secs: i64,
    49→    pub display_count: i32,
    50→    pub agent_version: Option<String>,  // Agent software version
    51→}
    52→
    53→/// Channel for sending frames from agent to viewers
    54→pub type FrameSender = broadcast::Sender<Vec<u8>>;
    55→pub type FrameReceiver = broadcast::Receiver<Vec<u8>>;

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
    pub agent_version: Option<String>,  // Agent software version
}

//! Session management for GuruConnect
//!
//! Manages active remote desktop sessions, tracking which agents
//! are connected and which viewers are watching them.

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;

/// Unique identifier for a session
pub type SessionId = Uuid;

/// Unique identifier for an agent
pub type AgentId = String;

/// Unique identifier for a viewer
pub type ViewerId = String;

/// Information about a connected viewer/technician
#[derive(Debug, Clone)]
pub struct ViewerInfo {
    pub id: ViewerId,
    pub name: String,
    pub connected_at: chrono::DateTime<chrono::Utc>,
}

/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
const HEARTBEAT_TIMEOUT_SECS: u64 = 90;

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
}

/// Channel for sending frames from agent to viewers
pub type FrameSender = broadcast::Sender<Vec<u8>>;
pub type FrameReceiver = broadcast::Receiver<Vec<u8>>;

/// Channel for sending input events from viewer to agent
pub type InputSender = tokio::sync::mpsc::Sender<Vec<u8>>;
pub type InputReceiver = tokio::sync::mpsc::Receiver<Vec<u8>>;

/// Internal session data with channels
struct SessionData {
    info: Session,
    /// Channel for video frames (agent -> viewers)
    frame_tx: FrameSender,
    /// Channel for input events (viewer -> agent)
    input_tx: InputSender,
    input_rx: Option<InputReceiver>,
    /// Map of connected viewers (id -> info)
    viewers: HashMap<ViewerId, ViewerInfo>,
    /// Instant for heartbeat tracking
    last_heartbeat_instant: Instant,
}

/// Manages all active sessions
#[derive(Clone)]
pub struct SessionManager {
    sessions: Arc<RwLock<HashMap<SessionId, SessionData>>>,
    agents: Arc<RwLock<HashMap<AgentId, SessionId>>>,
}

impl SessionManager {
    pub fn new() -> Self {
        Self {
            sessions: Arc::new(RwLock::new(HashMap::new())),
            agents: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Register a new agent and create a session
    /// 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
        let (frame_tx, _) = broadcast::channel(16); // Buffer 16 frames
        let (input_tx, input_rx) = tokio::sync::mpsc::channel(64); // Buffer 64 input events

        let now = chrono::Utc::now();
        let session = Session {
            id: session_id,
            agent_id: agent_id.clone(),
            agent_name,
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: true,
            is_persistent,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        let session_data = SessionData {
            info: session,
            frame_tx: frame_tx.clone(),
            input_tx,
            input_rx: None,
            viewers: HashMap::new(),
            last_heartbeat_instant: Instant::now(),
        };

        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id, session_data);

        let mut agents = self.agents.write().await;
        agents.insert(agent_id, session_id);

        (session_id, frame_tx, input_rx)
    }

    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
        }
    }

    /// Update heartbeat timestamp
    pub async fn update_heartbeat(&self, session_id: SessionId) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
        }
    }

    /// Check if a session has timed out (no heartbeat for too long)
    pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            session_data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS
        } else {
            true // Non-existent sessions are considered timed out
        }
    }

    /// Get sessions that have timed out
    pub async fn get_timed_out_sessions(&self) -> Vec<SessionId> {
        let sessions = self.sessions.read().await;
        sessions
            .iter()
            .filter(|(_, data)| data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS)
            .map(|(id, _)| *id)
            .collect()
    }

    /// Get a session by agent ID
    pub async fn get_session_by_agent(&self, agent_id: &str) -> Option<Session> {
        let agents = self.agents.read().await;
        let session_id = agents.get(agent_id)?;

        let sessions = self.sessions.read().await;
        sessions.get(session_id).map(|s| s.info.clone())
    }

    /// Get a session by session ID
    pub async fn get_session(&self, session_id: SessionId) -> Option<Session> {
        let sessions = self.sessions.read().await;
        sessions.get(&session_id).map(|s| s.info.clone())
    }

    /// Join a session as a viewer, returns channels and sends StartStream to agent
    pub async fn join_session(&self, session_id: SessionId, viewer_id: ViewerId, viewer_name: String) -> Option<(FrameReceiver, InputSender)> {
        let mut sessions = self.sessions.write().await;
        let session_data = sessions.get_mut(&session_id)?;

        let was_empty = session_data.viewers.is_empty();

        // Add viewer info
        let viewer_info = ViewerInfo {
            id: viewer_id.clone(),
            name: viewer_name.clone(),
            connected_at: chrono::Utc::now(),
        };
        session_data.viewers.insert(viewer_id.clone(), viewer_info);

        // Update session info
        session_data.info.viewer_count = session_data.viewers.len();
        session_data.info.viewers = session_data.viewers.values().cloned().collect();

        let frame_rx = session_data.frame_tx.subscribe();
        let input_tx = session_data.input_tx.clone();

        // If this is the first viewer, send StartStream to agent
        if was_empty {
            tracing::info!("Viewer {} ({}) joined session {}, sending StartStream", viewer_name, viewer_id, session_id);
            Self::send_start_stream_internal(session_data, &viewer_id).await;
        } else {
            tracing::info!("Viewer {} ({}) joined session {}", viewer_name, viewer_id, session_id);
        }

        Some((frame_rx, input_tx))
    }

    /// Internal helper to send StartStream message
    async fn send_start_stream_internal(session_data: &SessionData, viewer_id: &str) {
        use crate::proto;
        use prost::Message;

        let start_stream = proto::Message {
            payload: Some(proto::message::Payload::StartStream(proto::StartStream {
                viewer_id: viewer_id.to_string(),
                display_id: 0, // Primary display
            })),
        };

        let mut buf = Vec::new();
        if start_stream.encode(&mut buf).is_ok() {
            let _ = session_data.input_tx.send(buf).await;
        }
    }

    /// Leave a session as a viewer, sends StopStream if no viewers left
    pub async fn leave_session(&self, session_id: SessionId, viewer_id: &ViewerId) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            let viewer_name = session_data.viewers.get(viewer_id).map(|v| v.name.clone());
            session_data.viewers.remove(viewer_id);
            session_data.info.viewer_count = session_data.viewers.len();
            session_data.info.viewers = session_data.viewers.values().cloned().collect();

            // If no more viewers, send StopStream to agent
            if session_data.viewers.is_empty() {
                tracing::info!("Last viewer {} ({}) left session {}, sending StopStream",
                    viewer_name.as_deref().unwrap_or("unknown"), viewer_id, session_id);
                Self::send_stop_stream_internal(session_data, viewer_id).await;
            } else {
                tracing::info!("Viewer {} ({}) left session {}",
                    viewer_name.as_deref().unwrap_or("unknown"), viewer_id, session_id);
            }
        }
    }

    /// Internal helper to send StopStream message
    async fn send_stop_stream_internal(session_data: &SessionData, viewer_id: &str) {
        use crate::proto;
        use prost::Message;

        let stop_stream = proto::Message {
            payload: Some(proto::message::Payload::StopStream(proto::StopStream {
                viewer_id: viewer_id.to_string(),
            })),
        };

        let mut buf = Vec::new();
        if stop_stream.encode(&mut buf).is_ok() {
            let _ = session_data.input_tx.send(buf).await;
        }
    }

    /// 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.info.viewers.clear();
                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);
        }
    }

    /// Disconnect a session by sending a disconnect message to the agent
    /// Returns true if the message was sent successfully
    pub async fn disconnect_session(&self, session_id: SessionId, reason: &str) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            // Create disconnect message
            use crate::proto;
            use prost::Message;

            let disconnect_msg = proto::Message {
                payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                    reason: reason.to_string(),
                })),
            };

            let mut buf = Vec::new();
            if disconnect_msg.encode(&mut buf).is_ok() {
                // Send via input channel (will be forwarded to agent's WebSocket)
                if session_data.input_tx.send(buf).await.is_ok() {
                    return true;
                }
            }
        }
        false
    }

    /// List all active sessions
    pub async fn list_sessions(&self) -> Vec<Session> {
        let sessions = self.sessions.read().await;
        sessions.values().map(|s| s.info.clone()).collect()
    }

    /// Send an admin command to an agent (uninstall, restart, etc.)
    /// Returns true if the message was sent successfully
    pub async fn send_admin_command(&self, session_id: SessionId, command: crate::proto::AdminCommandType, reason: &str) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            if !session_data.info.is_online {
                tracing::warn!("Cannot send admin command to offline agent");
                return false;
            }

            use crate::proto;
            use prost::Message;

            let admin_cmd = proto::Message {
                payload: Some(proto::message::Payload::AdminCommand(proto::AdminCommand {
                    command: command as i32,
                    reason: reason.to_string(),
                })),
            };

            let mut buf = Vec::new();
            if admin_cmd.encode(&mut buf).is_ok() {
                if session_data.input_tx.send(buf).await.is_ok() {
                    tracing::info!("Sent admin command {:?} to session {}", command, session_id);
                    return true;
                }
            }
        }
        false
    }

    /// Remove an agent/machine from the session manager (for deletion)
    /// Returns the agent_id if found
    pub async fn remove_agent(&self, agent_id: &str) -> Option<SessionId> {
        let agents = self.agents.read().await;
        let session_id = agents.get(agent_id).copied()?;
        drop(agents);

        self.remove_session(session_id).await;
        Some(session_id)
    }
}

impl Default for SessionManager {
    fn default() -> Self {
        Self::new()
    }
}

impl SessionManager {
    /// Restore a machine as an offline session (called on startup from database)
    pub async fn restore_offline_machine(&self, agent_id: &str, hostname: &str) -> SessionId {
        let session_id = Uuid::new_v4();
        let now = chrono::Utc::now();

        let session = Session {
            id: session_id,
            agent_id: agent_id.to_string(),
            agent_name: hostname.to_string(),
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: false,  // Offline until agent reconnects
            is_persistent: true,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        // Create placeholder channels (will be replaced on reconnect)
        let (frame_tx, _) = broadcast::channel(16);
        let (input_tx, input_rx) = tokio::sync::mpsc::channel(64);

        let session_data = SessionData {
            info: session,
            frame_tx,
            input_tx,
            input_rx: Some(input_rx),
            viewers: HashMap::new(),
            last_heartbeat_instant: Instant::now(),
        };

        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id, session_data);

        let mut agents = self.agents.write().await;
        agents.insert(agent_id.to_string(), session_id);

        tracing::info!("Restored offline machine: {} ({})", hostname, agent_id);
        session_id
    }
}

+    pub agent_version: Option<String>,  // Agent software version


Now I need to update the `update_agent_status` method to also take agent_version, and I need to update the places where Session is created to initialize the new field.


Now update the update_agent_status method:


    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
        }
    }

    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
        agent_version: Option<String>,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
            if let Some(version) = agent_version {
                session_data.info.agent_version = Some(version);
            }
        }
    }


The file C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   158→
   159→        (session_id, frame_tx, input_rx)
   160→    }
   161→
   162→    /// Update agent status from heartbeat or status message
   163→    pub async fn update_agent_status(
   164→        &self,
   165→        session_id: SessionId,
   166→        os_version: Option<String>,
   167→        is_elevated: bool,
   168→        uptime_secs: i64,
   169→        display_count: i32,
   170→        is_streaming: bool,
   171→        agent_version: Option<String>,
   172→    ) {
   173→        let mut sessions = self.sessions.write().await;
   174→        if let Some(session_data) = sessions.get_mut(&session_id) {
   175→            session_data.info.last_heartbeat = chrono::Utc::now();
   176→            session_data.last_heartbeat_instant = Instant::now();
   177→            session_data.info.is_streaming = is_streaming;
   178→            if let Some(os) = os_version {
   179→                session_data.info.os_version = Some(os);
   180→            }
   181→            session_data.info.is_elevated = is_elevated;
   182→            session_data.info.uptime_secs = uptime_secs;
   183→            session_data.info.display_count = display_count;
   184→            if let Some(version) = agent_version {
   185→                session_data.info.agent_version = Some(version);
   186→            }
   187→        }
   188→    }
   189→
   190→    /// Update heartbeat timestamp
   191→    pub async fn update_heartbeat(&self, session_id: SessionId) {
   192→        let mut sessions = self.sessions.write().await;

    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
        agent_version: Option<String>,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
            if let Some(version) = agent_version {
                session_data.info.agent_version = Some(version);
            }
        }
    }

//! Session management for GuruConnect
//!
//! Manages active remote desktop sessions, tracking which agents
//! are connected and which viewers are watching them.

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;

/// Unique identifier for a session
pub type SessionId = Uuid;

/// Unique identifier for an agent
pub type AgentId = String;

/// Unique identifier for a viewer
pub type ViewerId = String;

/// Information about a connected viewer/technician
#[derive(Debug, Clone)]
pub struct ViewerInfo {
    pub id: ViewerId,
    pub name: String,
    pub connected_at: chrono::DateTime<chrono::Utc>,
}

/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
const HEARTBEAT_TIMEOUT_SECS: u64 = 90;

/// Session state
#[derive(Debug, Clone)]
pub struct Session {
    pub id: SessionId,
    pub agent_id: AgentId,
    pub agent_name: String,
    pub started_at: chrono::DateTime<chrono::Utc>,
    pub viewer_count: usize,
    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
    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>,
    pub is_elevated: bool,
    pub uptime_secs: i64,
    pub display_count: i32,
    pub agent_version: Option<String>,  // Agent software version
}

/// Channel for sending frames from agent to viewers
pub type FrameSender = broadcast::Sender<Vec<u8>>;
pub type FrameReceiver = broadcast::Receiver<Vec<u8>>;

/// Channel for sending input events from viewer to agent
pub type InputSender = tokio::sync::mpsc::Sender<Vec<u8>>;
pub type InputReceiver = tokio::sync::mpsc::Receiver<Vec<u8>>;

/// Internal session data with channels
struct SessionData {
    info: Session,
    /// Channel for video frames (agent -> viewers)
    frame_tx: FrameSender,
    /// Channel for input events (viewer -> agent)
    input_tx: InputSender,
    input_rx: Option<InputReceiver>,
    /// Map of connected viewers (id -> info)
    viewers: HashMap<ViewerId, ViewerInfo>,
    /// Instant for heartbeat tracking
    last_heartbeat_instant: Instant,
}

/// Manages all active sessions
#[derive(Clone)]
pub struct SessionManager {
    sessions: Arc<RwLock<HashMap<SessionId, SessionData>>>,
    agents: Arc<RwLock<HashMap<AgentId, SessionId>>>,
}

impl SessionManager {
    pub fn new() -> Self {
        Self {
            sessions: Arc::new(RwLock::new(HashMap::new())),
            agents: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Register a new agent and create a session
    /// 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
        let (frame_tx, _) = broadcast::channel(16); // Buffer 16 frames
        let (input_tx, input_rx) = tokio::sync::mpsc::channel(64); // Buffer 64 input events

        let now = chrono::Utc::now();
        let session = Session {
            id: session_id,
            agent_id: agent_id.clone(),
            agent_name,
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: true,
            is_persistent,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        let session_data = SessionData {
            info: session,
            frame_tx: frame_tx.clone(),
            input_tx,
            input_rx: None,
            viewers: HashMap::new(),
            last_heartbeat_instant: Instant::now(),
        };

        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id, session_data);

        let mut agents = self.agents.write().await;
        agents.insert(agent_id, session_id);

        (session_id, frame_tx, input_rx)
    }

    /// Update agent status from heartbeat or status message
    pub async fn update_agent_status(
        &self,
        session_id: SessionId,
        os_version: Option<String>,
        is_elevated: bool,
        uptime_secs: i64,
        display_count: i32,
        is_streaming: bool,
    ) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
            session_data.info.is_streaming = is_streaming;
            if let Some(os) = os_version {
                session_data.info.os_version = Some(os);
            }
            session_data.info.is_elevated = is_elevated;
            session_data.info.uptime_secs = uptime_secs;
            session_data.info.display_count = display_count;
        }
    }

    /// Update heartbeat timestamp
    pub async fn update_heartbeat(&self, session_id: SessionId) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            session_data.info.last_heartbeat = chrono::Utc::now();
            session_data.last_heartbeat_instant = Instant::now();
        }
    }

    /// Check if a session has timed out (no heartbeat for too long)
    pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            session_data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS
        } else {
            true // Non-existent sessions are considered timed out
        }
    }

    /// Get sessions that have timed out
    pub async fn get_timed_out_sessions(&self) -> Vec<SessionId> {
        let sessions = self.sessions.read().await;
        sessions
            .iter()
            .filter(|(_, data)| data.last_heartbeat_instant.elapsed().as_secs() > HEARTBEAT_TIMEOUT_SECS)
            .map(|(id, _)| *id)
            .collect()
    }

    /// Get a session by agent ID
    pub async fn get_session_by_agent(&self, agent_id: &str) -> Option<Session> {
        let agents = self.agents.read().await;
        let session_id = agents.get(agent_id)?;

        let sessions = self.sessions.read().await;
        sessions.get(session_id).map(|s| s.info.clone())
    }

    /// Get a session by session ID
    pub async fn get_session(&self, session_id: SessionId) -> Option<Session> {
        let sessions = self.sessions.read().await;
        sessions.get(&session_id).map(|s| s.info.clone())
    }

    /// Join a session as a viewer, returns channels and sends StartStream to agent
    pub async fn join_session(&self, session_id: SessionId, viewer_id: ViewerId, viewer_name: String) -> Option<(FrameReceiver, InputSender)> {
        let mut sessions = self.sessions.write().await;
        let session_data = sessions.get_mut(&session_id)?;

        let was_empty = session_data.viewers.is_empty();

        // Add viewer info
        let viewer_info = ViewerInfo {
            id: viewer_id.clone(),
            name: viewer_name.clone(),
            connected_at: chrono::Utc::now(),
        };
        session_data.viewers.insert(viewer_id.clone(), viewer_info);

        // Update session info
        session_data.info.viewer_count = session_data.viewers.len();
        session_data.info.viewers = session_data.viewers.values().cloned().collect();

        let frame_rx = session_data.frame_tx.subscribe();
        let input_tx = session_data.input_tx.clone();

        // If this is the first viewer, send StartStream to agent
        if was_empty {
            tracing::info!("Viewer {} ({}) joined session {}, sending StartStream", viewer_name, viewer_id, session_id);
            Self::send_start_stream_internal(session_data, &viewer_id).await;
        } else {
            tracing::info!("Viewer {} ({}) joined session {}", viewer_name, viewer_id, session_id);
        }

        Some((frame_rx, input_tx))
    }

    /// Internal helper to send StartStream message
    async fn send_start_stream_internal(session_data: &SessionData, viewer_id: &str) {
        use crate::proto;
        use prost::Message;

        let start_stream = proto::Message {
            payload: Some(proto::message::Payload::StartStream(proto::StartStream {
                viewer_id: viewer_id.to_string(),
                display_id: 0, // Primary display
            })),
        };

        let mut buf = Vec::new();
        if start_stream.encode(&mut buf).is_ok() {
            let _ = session_data.input_tx.send(buf).await;
        }
    }

    /// Leave a session as a viewer, sends StopStream if no viewers left
    pub async fn leave_session(&self, session_id: SessionId, viewer_id: &ViewerId) {
        let mut sessions = self.sessions.write().await;
        if let Some(session_data) = sessions.get_mut(&session_id) {
            let viewer_name = session_data.viewers.get(viewer_id).map(|v| v.name.clone());
            session_data.viewers.remove(viewer_id);
            session_data.info.viewer_count = session_data.viewers.len();
            session_data.info.viewers = session_data.viewers.values().cloned().collect();

            // If no more viewers, send StopStream to agent
            if session_data.viewers.is_empty() {
                tracing::info!("Last viewer {} ({}) left session {}, sending StopStream",
                    viewer_name.as_deref().unwrap_or("unknown"), viewer_id, session_id);
                Self::send_stop_stream_internal(session_data, viewer_id).await;
            } else {
                tracing::info!("Viewer {} ({}) left session {}",
                    viewer_name.as_deref().unwrap_or("unknown"), viewer_id, session_id);
            }
        }
    }

    /// Internal helper to send StopStream message
    async fn send_stop_stream_internal(session_data: &SessionData, viewer_id: &str) {
        use crate::proto;
        use prost::Message;

        let stop_stream = proto::Message {
            payload: Some(proto::message::Payload::StopStream(proto::StopStream {
                viewer_id: viewer_id.to_string(),
            })),
        };

        let mut buf = Vec::new();
        if stop_stream.encode(&mut buf).is_ok() {
            let _ = session_data.input_tx.send(buf).await;
        }
    }

    /// 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.info.viewers.clear();
                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);
        }
    }

    /// Disconnect a session by sending a disconnect message to the agent
    /// Returns true if the message was sent successfully
    pub async fn disconnect_session(&self, session_id: SessionId, reason: &str) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            // Create disconnect message
            use crate::proto;
            use prost::Message;

            let disconnect_msg = proto::Message {
                payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                    reason: reason.to_string(),
                })),
            };

            let mut buf = Vec::new();
            if disconnect_msg.encode(&mut buf).is_ok() {
                // Send via input channel (will be forwarded to agent's WebSocket)
                if session_data.input_tx.send(buf).await.is_ok() {
                    return true;
                }
            }
        }
        false
    }

    /// List all active sessions
    pub async fn list_sessions(&self) -> Vec<Session> {
        let sessions = self.sessions.read().await;
        sessions.values().map(|s| s.info.clone()).collect()
    }

    /// Send an admin command to an agent (uninstall, restart, etc.)
    /// Returns true if the message was sent successfully
    pub async fn send_admin_command(&self, session_id: SessionId, command: crate::proto::AdminCommandType, reason: &str) -> bool {
        let sessions = self.sessions.read().await;
        if let Some(session_data) = sessions.get(&session_id) {
            if !session_data.info.is_online {
                tracing::warn!("Cannot send admin command to offline agent");
                return false;
            }

            use crate::proto;
            use prost::Message;

            let admin_cmd = proto::Message {
                payload: Some(proto::message::Payload::AdminCommand(proto::AdminCommand {
                    command: command as i32,
                    reason: reason.to_string(),
                })),
            };

            let mut buf = Vec::new();
            if admin_cmd.encode(&mut buf).is_ok() {
                if session_data.input_tx.send(buf).await.is_ok() {
                    tracing::info!("Sent admin command {:?} to session {}", command, session_id);
                    return true;
                }
            }
        }
        false
    }

    /// Remove an agent/machine from the session manager (for deletion)
    /// Returns the agent_id if found
    pub async fn remove_agent(&self, agent_id: &str) -> Option<SessionId> {
        let agents = self.agents.read().await;
        let session_id = agents.get(agent_id).copied()?;
        drop(agents);

        self.remove_session(session_id).await;
        Some(session_id)
    }
}

impl Default for SessionManager {
    fn default() -> Self {
        Self::new()
    }
}

impl SessionManager {
    /// Restore a machine as an offline session (called on startup from database)
    pub async fn restore_offline_machine(&self, agent_id: &str, hostname: &str) -> SessionId {
        let session_id = Uuid::new_v4();
        let now = chrono::Utc::now();

        let session = Session {
            id: session_id,
            agent_id: agent_id.to_string(),
            agent_name: hostname.to_string(),
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: false,  // Offline until agent reconnects
            is_persistent: true,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        // Create placeholder channels (will be replaced on reconnect)
        let (frame_tx, _) = broadcast::channel(16);
        let (input_tx, input_rx) = tokio::sync::mpsc::channel(64);

        let session_data = SessionData {
            info: session,
            frame_tx,
            input_tx,
            input_rx: Some(input_rx),
            viewers: HashMap::new(),
            last_heartbeat_instant: Instant::now(),
        };

        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id, session_data);

        let mut agents = self.agents.write().await;
        agents.insert(agent_id.to_string(), session_id);

        tracing::info!("Restored offline machine: {} ({})", hostname, agent_id);
        session_id
    }
}

+            if let Some(version) = agent_version {

+                session_data.info.agent_version = Some(version);


Now I need to update the relay module to pass the agent_version when calling update_agent_status. I also need to find where Session structs are created and add the agent_version field.


Now update the relay module to pass agent_version:


                            Some(proto::message::Payload::AgentStatus(status)) => {
                                // Update session with agent status
                                sessions_status.update_agent_status(
                                    session_id,
                                    Some(status.os_version.clone()),
                                    status.is_elevated,
                                    status.uptime_secs,
                                    status.display_count,
                                    status.is_streaming,
                                ).await;
                                info!("Agent status update: {} - streaming={}, uptime={}s",
                                    status.hostname, status.is_streaming, status.uptime_secs);
                            }

                            Some(proto::message::Payload::AgentStatus(status)) => {
                                // Update session with agent status
                                let agent_version = if status.agent_version.is_empty() {
                                    None
                                } else {
                                    Some(status.agent_version.clone())
                                };
                                sessions_status.update_agent_status(
                                    session_id,
                                    Some(status.os_version.clone()),
                                    status.is_elevated,
                                    status.uptime_secs,
                                    status.display_count,
                                    status.is_streaming,
                                    agent_version.clone(),
                                ).await;

                                // Update version in database if present
                                if let (Some(ref db), Some(ref version)) = (&db, &agent_version) {
                                    let _ = crate::db::releases::update_machine_version(db.pool(), &agent_id, version).await;
                                }

                                info!("Agent status update: {} - streaming={}, uptime={}s, version={:?}",
                                    status.hostname, status.is_streaming, status.uptime_secs, agent_version);
                            }


The file C:\Users\MikeSwanson\Claude\guru-connect\server\src\relay\mod.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   305→                                // Broadcast chat message to all viewers
   306→                                info!("Chat from client: {}", chat.content);
   307→                                let _ = frame_tx.send(data.to_vec());
   308→                            }
   309→                            Some(proto::message::Payload::AgentStatus(status)) => {
   310→                                // Update session with agent status
   311→                                let agent_version = if status.agent_version.is_empty() {
   312→                                    None
   313→                                } else {
   314→                                    Some(status.agent_version.clone())
   315→                                };
   316→                                sessions_status.update_agent_status(
   317→                                    session_id,
   318→                                    Some(status.os_version.clone()),
   319→                                    status.is_elevated,
   320→                                    status.uptime_secs,
   321→                                    status.display_count,
   322→                                    status.is_streaming,
   323→                                    agent_version.clone(),
   324→                                ).await;
   325→
   326→                                // Update version in database if present
   327→                                if let (Some(ref db), Some(ref version)) = (&db, &agent_version) {
   328→                                    let _ = crate::db::releases::update_machine_version(db.pool(), &agent_id, version).await;
   329→                                }
   330→
   331→                                info!("Agent status update: {} - streaming={}, uptime={}s, version={:?}",
   332→                                    status.hostname, status.is_streaming, status.uptime_secs, agent_version);
   333→                            }
   334→                            Some(proto::message::Payload::Heartbeat(_)) => {
   335→                                // Update heartbeat timestamp
   336→                                sessions_status.update_heartbeat(session_id).await;
   337→                            }

                            Some(proto::message::Payload::AgentStatus(status)) => {
                                // Update session with agent status
                                let agent_version = if status.agent_version.is_empty() {
                                    None
                                } else {
                                    Some(status.agent_version.clone())
                                };
                                sessions_status.update_agent_status(
                                    session_id,
                                    Some(status.os_version.clone()),
                                    status.is_elevated,
                                    status.uptime_secs,
                                    status.display_count,
                                    status.is_streaming,
                                    agent_version.clone(),
                                ).await;

                                // Update version in database if present
                                if let (Some(ref db), Some(ref version)) = (&db, &agent_version) {
                                    let _ = crate::db::releases::update_machine_version(db.pool(), &agent_id, version).await;
                                }

                                info!("Agent status update: {} - streaming={}, uptime={}s, version={:?}",
                                    status.hostname, status.is_streaming, status.uptime_secs, agent_version);
                            }

//! WebSocket relay handlers
//!
//! Handles WebSocket connections from agents and viewers,
//! relaying video frames and input events between them.

use axum::{
    extract::{
        ws::{Message, WebSocket, WebSocketUpgrade},
        Query, State,
    },
    response::IntoResponse,
    http::StatusCode,
};
use futures_util::{SinkExt, StreamExt};
use prost::Message as ProstMessage;
use serde::Deserialize;
use tracing::{error, info, warn};
use uuid::Uuid;

use crate::proto;
use crate::session::SessionManager;
use crate::db::{self, Database};
use crate::AppState;

#[derive(Debug, Deserialize)]
pub struct AgentParams {
    agent_id: String,
    #[serde(default)]
    agent_name: Option<String>,
    #[serde(default)]
    support_code: Option<String>,
    #[serde(default)]
    hostname: Option<String>,
    /// API key for persistent (managed) agents
    #[serde(default)]
    api_key: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct ViewerParams {
    session_id: String,
    #[serde(default = "default_viewer_name")]
    viewer_name: String,
    /// JWT token for authentication (required)
    #[serde(default)]
    token: Option<String>,
}

fn default_viewer_name() -> String {
    "Technician".to_string()
}

/// WebSocket handler for agent connections
pub async fn agent_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<AgentParams>,
) -> Result<impl IntoResponse, StatusCode> {
    let agent_id = params.agent_id.clone();
    let agent_name = params.hostname.clone().or(params.agent_name.clone()).unwrap_or_else(|| agent_id.clone());
    let support_code = params.support_code.clone();
    let api_key = params.api_key.clone();

    // SECURITY: Agent must provide either a support code OR an API key
    // Support code = ad-hoc support session (technician generated code)
    // API key = persistent managed agent

    if support_code.is_none() && api_key.is_none() {
        warn!("Agent connection rejected: {} - no support code or API key", agent_id);
        return Err(StatusCode::UNAUTHORIZED);
    }

    // Validate support code if provided
    if let Some(ref code) = support_code {
        // Check if it's a valid, pending support code
        let code_info = state.support_codes.get_status(code).await;
        if code_info.is_none() {
            warn!("Agent connection rejected: {} - invalid support code {}", agent_id, code);
            return Err(StatusCode::UNAUTHORIZED);
        }
        let status = code_info.unwrap();
        if status != "pending" && status != "connected" {
            warn!("Agent connection rejected: {} - support code {} has status {}", agent_id, code, status);
            return Err(StatusCode::UNAUTHORIZED);
        }
        info!("Agent {} authenticated via support code {}", agent_id, code);
    }

    // Validate API key if provided (for persistent agents)
    if let Some(ref key) = api_key {
        // For now, we'll accept API keys that match the JWT secret or a configured agent key
        // In production, this should validate against a database of registered agents
        if !validate_agent_api_key(&state, key).await {
            warn!("Agent connection rejected: {} - invalid API key", agent_id);
            return Err(StatusCode::UNAUTHORIZED);
        }
        info!("Agent {} authenticated via API key", agent_id);
    }

    let sessions = state.sessions.clone();
    let support_codes = state.support_codes.clone();
    let db = state.db.clone();

    Ok(ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, db, agent_id, agent_name, support_code)))
}

/// Validate an agent API key
async fn validate_agent_api_key(state: &AppState, api_key: &str) -> bool {
    // Check if API key is a valid JWT (allows using dashboard token for testing)
    if state.jwt_config.validate_token(api_key).is_ok() {
        return true;
    }

    // Check against configured agent API key if set
    if let Some(ref configured_key) = state.agent_api_key {
        if api_key == configured_key {
            return true;
        }
    }

    // In future: validate against database of registered agents
    false
}

/// WebSocket handler for viewer connections
pub async fn viewer_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<ViewerParams>,
) -> Result<impl IntoResponse, StatusCode> {
    // Require JWT token for viewers
    let token = params.token.ok_or_else(|| {
        warn!("Viewer connection rejected: missing token");
        StatusCode::UNAUTHORIZED
    })?;

    // Validate the token
    let claims = state.jwt_config.validate_token(&token).map_err(|e| {
        warn!("Viewer connection rejected: invalid token: {}", e);
        StatusCode::UNAUTHORIZED
    })?;

    info!("Viewer {} authenticated via JWT", claims.username);

    let session_id = params.session_id;
    let viewer_name = params.viewer_name;
    let sessions = state.sessions.clone();
    let db = state.db.clone();

    Ok(ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, db, session_id, viewer_name)))
}

/// Handle an agent WebSocket connection
async fn handle_agent_connection(
    socket: WebSocket,
    sessions: SessionManager,
    support_codes: crate::support_codes::SupportCodeManager,
    db: Option<Database>,
    agent_id: String,
    agent_name: String,
    support_code: Option<String>,
) {
    info!("Agent connected: {} ({})", agent_name, agent_id);

    let (mut ws_sender, mut ws_receiver) = socket.split();

    // If a support code was provided, check if it's valid
    if let Some(ref code) = support_code {
        // Check if the code is cancelled or invalid
        if support_codes.is_cancelled(code).await {
            warn!("Agent tried to connect with cancelled code: {}", code);
            // Send disconnect message to agent
            let disconnect_msg = proto::Message {
                payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                    reason: "Support session was cancelled by technician".to_string(),
                })),
            };
            let mut buf = Vec::new();
            if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
                let _ = ws_sender.send(Message::Binary(buf.into())).await;
            }
            let _ = ws_sender.close().await;
            return;
        }
    }

    // Register the agent and get channels
    // 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);

    // Database: upsert machine and create session record
    let machine_id = if let Some(ref db) = db {
        match db::machines::upsert_machine(db.pool(), &agent_id, &agent_name, is_persistent).await {
            Ok(machine) => {
                // Create session record
                let _ = db::sessions::create_session(
                    db.pool(),
                    session_id,
                    machine.id,
                    support_code.is_some(),
                    support_code.as_deref(),
                ).await;

                // Log session started event
                let _ = db::events::log_event(
                    db.pool(),
                    session_id,
                    db::events::EventTypes::SESSION_STARTED,
                    None, None, None, None,
                ).await;

                Some(machine.id)
            }
            Err(e) => {
                warn!("Failed to upsert machine in database: {}", e);
                None
            }
        }
    } else {
        None
    };

    // If a support code was provided, mark it as connected
    if let Some(ref code) = support_code {
        info!("Linking support code {} to session {}", code, session_id);
        support_codes.mark_connected(code, Some(agent_name.clone()), Some(agent_id.clone())).await;
        support_codes.link_session(code, session_id).await;

        // Database: update support code
        if let Some(ref db) = db {
            let _ = db::support_codes::mark_code_connected(
                db.pool(),
                code,
                Some(session_id),
                Some(&agent_name),
                Some(&agent_id),
            ).await;
        }
    }

    // Use Arc<Mutex> for sender so we can use it from multiple places
    let ws_sender = std::sync::Arc::new(tokio::sync::Mutex::new(ws_sender));
    let ws_sender_input = ws_sender.clone();
    let ws_sender_cancel = ws_sender.clone();

    // Task to forward input events from viewers to agent
    let input_forward = tokio::spawn(async move {
        while let Some(input_data) = input_rx.recv().await {
            let mut sender = ws_sender_input.lock().await;
            if sender.send(Message::Binary(input_data.into())).await.is_err() {
                break;
            }
        }
    });

    let sessions_cleanup = sessions.clone();
    let sessions_status = sessions.clone();
    let support_codes_cleanup = support_codes.clone();
    let support_code_cleanup = support_code.clone();
    let support_code_check = support_code.clone();
    let support_codes_check = support_codes.clone();

    // Task to check for cancellation every 2 seconds
    let cancel_check = tokio::spawn(async move {
        let mut interval = tokio::time::interval(std::time::Duration::from_secs(2));
        loop {
            interval.tick().await;
            if let Some(ref code) = support_code_check {
                if support_codes_check.is_cancelled(code).await {
                    info!("Support code {} was cancelled, disconnecting agent", code);
                    // Send disconnect message
                    let disconnect_msg = proto::Message {
                        payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
                            reason: "Support session was cancelled by technician".to_string(),
                        })),
                    };
                    let mut buf = Vec::new();
                    if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
                        let mut sender = ws_sender_cancel.lock().await;
                        let _ = sender.send(Message::Binary(buf.into())).await;
                        let _ = sender.close().await;
                    }
                    break;
                }
            }
        }
    });

    // Main loop: receive messages from agent
    while let Some(msg) = ws_receiver.next().await {
        match msg {
            Ok(Message::Binary(data)) => {
                // Try to decode as protobuf message
                match proto::Message::decode(data.as_ref()) {
                    Ok(proto_msg) => {
                        match &proto_msg.payload {
                            Some(proto::message::Payload::VideoFrame(_)) => {
                                // Broadcast frame to all viewers (only sent when streaming)
                                let _ = frame_tx.send(data.to_vec());
                            }
                            Some(proto::message::Payload::ChatMessage(chat)) => {
                                // Broadcast chat message to all viewers
                                info!("Chat from client: {}", chat.content);
                                let _ = frame_tx.send(data.to_vec());
                            }
                            Some(proto::message::Payload::AgentStatus(status)) => {
                                // Update session with agent status
                                sessions_status.update_agent_status(
                                    session_id,
                                    Some(status.os_version.clone()),
                                    status.is_elevated,
                                    status.uptime_secs,
                                    status.display_count,
                                    status.is_streaming,
                                ).await;
                                info!("Agent status update: {} - streaming={}, uptime={}s",
                                    status.hostname, status.is_streaming, status.uptime_secs);
                            }
                            Some(proto::message::Payload::Heartbeat(_)) => {
                                // Update heartbeat timestamp
                                sessions_status.update_heartbeat(session_id).await;
                            }
                            Some(proto::message::Payload::HeartbeatAck(_)) => {
                                // Agent acknowledged our heartbeat
                                sessions_status.update_heartbeat(session_id).await;
                            }
                            _ => {}
                        }
                    }
                    Err(e) => {
                        warn!("Failed to decode agent message: {}", e);
                    }
                }
            }
            Ok(Message::Close(_)) => {
                info!("Agent disconnected: {}", agent_id);
                break;
            }
            Ok(Message::Ping(data)) => {
                // Pong is handled automatically by axum
                let _ = data;
            }
            Ok(_) => {}
            Err(e) => {
                error!("WebSocket error from agent {}: {}", agent_id, e);
                break;
            }
        }
    }

    // Cleanup
    input_forward.abort();
    cancel_check.abort();
    // Mark agent as disconnected (persistent agents stay in list as offline)
    sessions_cleanup.mark_agent_disconnected(session_id).await;

    // Database: end session and mark machine offline
    if let Some(ref db) = db {
        // End the session record
        let _ = db::sessions::end_session(db.pool(), session_id, "ended").await;

        // Mark machine as offline
        let _ = db::machines::mark_machine_offline(db.pool(), &agent_id).await;

        // Log session ended event
        let _ = db::events::log_event(
            db.pool(),
            session_id,
            db::events::EventTypes::SESSION_ENDED,
            None, None, None, None,
        ).await;
    }

    // Mark support code as completed if one was used (unless cancelled)
    if let Some(ref code) = support_code_cleanup {
        if !support_codes_cleanup.is_cancelled(code).await {
            support_codes_cleanup.mark_completed(code).await;

            // Database: mark code as completed
            if let Some(ref db) = db {
                let _ = db::support_codes::mark_code_completed(db.pool(), code).await;
            }

            info!("Support code {} marked as completed", code);
        }
    }

    info!("Session {} ended", session_id);
}

/// Handle a viewer WebSocket connection
async fn handle_viewer_connection(
    socket: WebSocket,
    sessions: SessionManager,
    db: Option<Database>,
    session_id_str: String,
    viewer_name: String,
) {
    // Parse session ID
    let session_id = match uuid::Uuid::parse_str(&session_id_str) {
        Ok(id) => id,
        Err(_) => {
            warn!("Invalid session ID: {}", session_id_str);
            return;
        }
    };

    // Generate unique viewer ID
    let viewer_id = Uuid::new_v4().to_string();

    // Join the session (this sends StartStream to agent if first viewer)
    let (mut frame_rx, input_tx) = match sessions.join_session(session_id, viewer_id.clone(), viewer_name.clone()).await {
        Some(channels) => channels,
        None => {
            warn!("Session not found: {}", session_id);
            return;
        }
    };

    info!("Viewer {} ({}) joined session: {}", viewer_name, viewer_id, session_id);

    // Database: log viewer joined event
    if let Some(ref db) = db {
        let _ = db::events::log_event(
            db.pool(),
            session_id,
            db::events::EventTypes::VIEWER_JOINED,
            Some(&viewer_id),
            Some(&viewer_name),
            None, None,
        ).await;
    }

    let (mut ws_sender, mut ws_receiver) = socket.split();

    // Task to forward frames from agent to this viewer
    let frame_forward = tokio::spawn(async move {
        while let Ok(frame_data) = frame_rx.recv().await {
            if ws_sender.send(Message::Binary(frame_data.into())).await.is_err() {
                break;
            }
        }
    });

    let sessions_cleanup = sessions.clone();
    let viewer_id_cleanup = viewer_id.clone();
    let viewer_name_cleanup = viewer_name.clone();

    // Main loop: receive input from viewer and forward to agent
    while let Some(msg) = ws_receiver.next().await {
        match msg {
            Ok(Message::Binary(data)) => {
                // Try to decode as protobuf message
                match proto::Message::decode(data.as_ref()) {
                    Ok(proto_msg) => {
                        match &proto_msg.payload {
                            Some(proto::message::Payload::MouseEvent(_)) |
                            Some(proto::message::Payload::KeyEvent(_)) |
                            Some(proto::message::Payload::SpecialKey(_)) => {
                                // Forward input to agent
                                let _ = input_tx.send(data.to_vec()).await;
                            }
                            Some(proto::message::Payload::ChatMessage(chat)) => {
                                // Forward chat message to agent
                                info!("Chat from technician: {}", chat.content);
                                let _ = input_tx.send(data.to_vec()).await;
                            }
                            _ => {}
                        }
                    }
                    Err(e) => {
                        warn!("Failed to decode viewer message: {}", e);
                    }
                }
            }
            Ok(Message::Close(_)) => {
                info!("Viewer {} disconnected from session: {}", viewer_id, session_id);
                break;
            }
            Ok(_) => {}
            Err(e) => {
                error!("WebSocket error from viewer {}: {}", viewer_id, e);
                break;
            }
        }
    }

    // Cleanup (this sends StopStream to agent if last viewer)
    frame_forward.abort();
    sessions_cleanup.leave_session(session_id, &viewer_id_cleanup).await;

    // Database: log viewer left event
    if let Some(ref db) = db {
        let _ = db::events::log_event(
            db.pool(),
            session_id,
            db::events::EventTypes::VIEWER_LEFT,
            Some(&viewer_id_cleanup),
            Some(&viewer_name_cleanup),
            None, None,
        ).await;
    }

    info!("Viewer {} left session: {}", viewer_id_cleanup, session_id);
}

+                                let agent_version = if status.agent_version.is_empty() {

+                                    None

+                                } else {

+                                    Some(status.agent_version.clone())

+                                    agent_version.clone(),

-                                info!("Agent status update: {} - streaming={}, uptime={}s",

-                                    status.hostname, status.is_streaming, status.uptime_secs);

+                                // Update version in database if present

+                                if let (Some(ref db), Some(ref version)) = (&db, &agent_version) {

+                                    let _ = crate::db::releases::update_machine_version(db.pool(), &agent_id, version).await;

+                                info!("Agent status update: {} - streaming={}, uptime={}s, version={:?}",

+                                    status.hostname, status.is_streaming, status.uptime_secs, agent_version);


Now I need to find where Session structs are created and add the agent_version field. Let me search for Session initialization.


Now I need to find where Session structs are initialized and add the agent_version field:


C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs:34:pub struct Session {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-35-    pub id: SessionId,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-36-    pub agent_id: AgentId,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-37-    pub agent_name: String,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-38-    pub started_at: chrono::DateTime<chrono::Utc>,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-39-    pub viewer_count: usize,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-40-    pub viewers: Vec<ViewerInfo>,  // List of connected technicians
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-41-    pub is_streaming: bool,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-42-    pub is_online: bool,  // Whether agent is currently connected
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-43-    pub is_persistent: bool,  // Persistent agent (no support code) vs support session
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-44-    pub last_heartbeat: chrono::DateTime<chrono::Utc>,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-45-    // Agent status info
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-46-    pub os_version: Option<String>,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-47-    pub is_elevated: bool,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-48-    pub uptime_secs: i64,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-49-    pub display_count: i32,
--
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs:127:        let session = Session {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-128-            id: session_id,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-129-            agent_id: agent_id.clone(),
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-130-            agent_name,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-131-            started_at: now,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-132-            viewer_count: 0,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-133-            viewers: Vec::new(),
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-134-            is_streaming: false,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-135-            is_online: true,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-136-            is_persistent,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-137-            last_heartbeat: now,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-138-            os_version: None,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-139-            is_elevated: false,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-140-            uptime_secs: 0,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-141-            display_count: 1,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-142-        };
--
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs:447:        let session = Session {
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-448-            id: session_id,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-449-            agent_id: agent_id.to_string(),
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-450-            agent_name: hostname.to_string(),
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-451-            started_at: now,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-452-            viewer_count: 0,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-453-            viewers: Vec::new(),
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-454-            is_streaming: false,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-455-            is_online: false,  // Offline until agent reconnects
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-456-            is_persistent: true,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-457-            last_heartbeat: now,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-458-            os_version: None,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-459-            is_elevated: false,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-460-            uptime_secs: 0,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-461-            display_count: 1,
C:\Users\MikeSwanson\Claude\guru-connect\server\src\session\mod.rs-462-        };


I see two places where Session is created - line 127 and line 447. I need to add `agent_version: None` to both of those.


        let session = Session {
            id: session_id,
            agent_id: agent_id.clone(),
            agent_name,
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: true,
            is_persistent,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
        };

        let session = Session {
            id: session_id,
            agent_id: agent_id.clone(),
            agent_name,
            started_at: now,
            viewer_count: 0,
            viewers: Vec::new(),
            is_streaming: false,
            is_online: true,
            is_persistent,
            last_heartbeat: now,
            os_version: None,
            is_elevated: false,
            uptime_secs: 0,
            display_count: 1,
            agent_version: None,
        };