This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:
Analysis:
Let me chronologically analyze the conversation:

1. **Initial Request**: User wants to test a permanent session on a machine with heavy video activity for GuruConnect project. They specified:
   - Organization: Pavon
   - Site: Raiders Reef
   - Tag: Geovision

2. **Database Schema Updates**: Added organization/site/tags columns to connect_machines table on the server.

3. **Protobuf Updates**: Modified `proto/guruconnect.proto` to add organization, site, and tags fields to AgentStatus message.

4. **Agent Updates**: Modified `agent/src/session/mod.rs` to include organization/site/tags in AgentStatus messages sent to server.

5. **Server Updates**:
   - Modified `server/src/session/mod.rs` to add organization/site/tags to Session struct and update_agent_status function
   - Modified `server/src/relay/mod.rs` to pass org/site/tags to update_agent_status
   - Modified `server/src/db/machines.rs` to add update_machine_metadata function

6. **Custom Agent Creation**: Created a custom agent binary with embedded config for Pavon/Raiders Reef/Geovision using Python script that appends GURUCONFIG magic marker + JSON config to the exe.

7. **Server Issues**: Server wasn't running, had to restart with AGENT_API_KEY environment variable set.

8. **Agent Connected Successfully**: DESKTOP-I66IM5Q connected with org=Pavon, site=Raiders Reef.

9. **Dashboard Issues**:
   - Access tab showed 0 machines because loadMachines() was missing Authorization header
   - Fixed by adding token to fetch call

10. **Viewer Issues**:
    - Web viewer showed "Not logged in" - fixed by changing localStorage key from 'authToken' to 'guruconnect_token'
    - Also fixed user key from 'user' to 'guruconnect_user'
    - Had to add cache-busting parameter to viewer URL

11. **Native Viewer Issues**:
    - Showed "Invalid URL: Missing session ID"
    - Fixed by having user download updated guruconnect.exe and run `install`

12. **Current Issue**: Native viewer window opens (shows "GuruConnect Viewer" title bar) but displays white/blank screen. Server logs show:
    - Viewer authenticated and joined session
    - Server sent StartStream to agent
    - Agent stopped sending status updates after StartStream (last update 3+ minutes ago)
    - Agent may have crashed when receiving StartStream command

Key files modified:
- proto/guruconnect.proto
- agent/src/session/mod.rs
- agent/src/install.rs (added debug logging)
- server/src/session/mod.rs
- server/src/relay/mod.rs
- server/src/db/machines.rs
- server/static/dashboard.html (auth headers, cache busting)
- server/static/viewer.html (localStorage key fixes)

Summary:
1. Primary Request and Intent:
   The user wanted to test GuruConnect with a permanent/persistent agent session on a machine with heavy video activity (Geovision system). They specified the organizational structure:
   - Organization: Pavon
   - Site: Raiders Reef
   - Tag: Geovision

   The goal was to deploy a custom agent with embedded config and test how the remote desktop performs under video-heavy conditions.

2. Key Technical Concepts:
   - GuruConnect remote desktop solution architecture (Agent → Server → Viewer)
   - Protocol Buffers for message serialization
   - WebSocket connections for real-time communication
   - JWT authentication for dashboard/viewer
   - API key authentication for persistent agents
   - localStorage for client-side token storage
   - Windows protocol handlers (guruconnect://)
   - Custom binary creation with embedded JSON config (GURUCONFIG magic marker)
   - PostgreSQL database for machine/session tracking

3. Files and Code Sections:
   - **proto/guruconnect.proto**
     - Added organization/site/tags to AgentStatus message for machine grouping
     ```protobuf
     message AgentStatus {
         string hostname = 1;
         string os_version = 2;
         bool is_elevated = 3;
         int64 uptime_secs = 4;
         int32 display_count = 5;
         bool is_streaming = 6;
         string agent_version = 7;
         string organization = 8;            // Company/organization name
         string site = 9;                    // Site/location name
         repeated string tags = 10;          // Tags for categorization
     }
     ```

   - **agent/src/session/mod.rs**
     - Modified send_status() to include org/site/tags from config
     ```rust
     let status = AgentStatus {
         hostname: self.hostname.clone(),
         os_version: std::env::consts::OS.to_string(),
         is_elevated: self.is_elevated,
         uptime_secs: self.start_time.elapsed().as_secs() as i64,
         display_count: self.get_display_count(),
         is_streaming: self.state == SessionState::Streaming,
         agent_version: crate::build_info::short_version(),
         organization: self.config.company.clone().unwrap_or_default(),
         site: self.config.site.clone().unwrap_or_default(),
         tags: self.config.tags.clone(),
     };
     ```

   - **server/src/session/mod.rs**
     - Added organization/site/tags fields to Session struct
     - Updated update_agent_status() function signature to accept new parameters

   - **server/src/db/machines.rs**
     - Added update_machine_metadata() function
     ```rust
     pub async fn update_machine_metadata(
         pool: &PgPool,
         agent_id: &str,
         organization: Option<&str>,
         site: Option<&str>,
         tags: &[String],
     ) -> Result<(), sqlx::Error>
     ```

   - **server/static/dashboard.html**
     - Fixed loadMachines() to include Authorization header
     - Added cache busting to viewer URL
     ```javascript
     const token = localStorage.getItem("guruconnect_token");
     const response = await fetch("/api/sessions", {
         headers: { "Authorization": "Bearer " + token }
     });
     ```
     ```javascript
     const viewerUrl = "/viewer.html?v=2&session_id=" + connectSessionId;
     ```

   - **server/static/viewer.html**
     - Fixed localStorage keys from 'authToken'/'user' to 'guruconnect_token'/'guruconnect_user'

   - **agent/src/install.rs**
     - Added debug logging to parse_protocol_url() for troubleshooting native viewer URL issues

4. Errors and fixes:
   - **Server not running**: Restarted with DATABASE_URL and AGENT_API_KEY environment variables
   - **Agent auth failed**: Set AGENT_API_KEY="permanent-agent-key" on server to match agent config
   - **Dashboard Access tab showing 0 machines**: loadMachines() fetch was missing Authorization header - fixed by adding token to headers
   - **Web viewer "Not logged in"**: localStorage key mismatch - viewer used 'authToken' but dashboard stored as 'guruconnect_token' - fixed by updating viewer.html
   - **Native viewer "Missing session ID"**: Old protocol handler binary - fixed by having user download new guruconnect.exe and run `install`
   - **Browser caching old viewer.html**: Added cache-busting parameter `?v=2&` to viewer URL
   - **Proto file edits failing**: Used Python scripts instead of direct Edit tool due to file modification detection issues

5. Problem Solving:
   - Successfully deployed custom agent with embedded org/site/tags config
   - Agent connects and reports organization metadata correctly
   - Web viewer now works after auth token fixes
   - Native viewer launches but shows blank white screen
   - **Ongoing issue**: Agent appears to crash or stop communicating after receiving StartStream command - last status update was 3+ minutes before current time

6. All user messages:
   - "Returning to guruconnect project, I have a machine I'd like to test a permanent session, it always has a lot of video happening on it, want to see how it runs there"
   - "Organization is Pavon, Site is Raiders Reef, Tag is Geovision"
   - "I forgot admin password"
   - "I ran the installer on the machine, does not show up on access list."
   - "restarted"
   - "Not showing in access list"
   - "no change. In F12 mode nothing in console, but https://connect.azcomputerguru.com/viewer.html?session_id=de42557c-14cb-4cba-ac0d-0aa15f4c5c7c listed as URI in debugger"
   - "it works now" [interrupted] "Still not working" (with screenshot showing 0 machines)
   - "Now I can't click on anything. Says 'loading' on the right." (with screenshot)
   - "Viewer doesn't work now." (with screenshots showing "Not logged in" and "Invalid URL: Missing session ID")
   - "still same errors for both native and web viewer"
   - "no" (viewer still not working)
   - "No change in behavior."
   - (Provided browser console output showing WebSocket connected then closed: 1006)
   - "no change. In F12 mode nothing in console..."
   - "URL connect.azcomputerguru.com/viewer.html?session_id=de42557c-14cb-4cba-ac0d-0aa15f4c5c7c - Message says 'Not logged in'"
   - "Web viewer works. Native pops this error:" (Invalid URL: Missing session ID)
   - "Give a wget command to get that"
   - "wget https://connect.azcomputerguru.com/downloads/guruconnect.exe -O guruconnect.exe"
   - "done, trying native viewer now" (with screenshot showing white viewer window)
   - "Yes, still showing white"

7. Pending Tasks:
   - Diagnose why agent stops communicating after receiving StartStream command
   - Get video streaming working between agent and viewer
   - Test performance with heavy video activity on target machine

8. Current Work:
   The native viewer now launches successfully (shows "GuruConnect Viewer" window) but displays a blank white screen. Server logs show:
   - Viewer authenticates via JWT successfully
   - Viewer joins session successfully
   - Server sends StartStream command to agent
   - Agent stops sending status updates after StartStream (crashed?)

   Last server log entries:
   ```
   [2026-01-01T19:41:53.588065Z] INFO Viewer admin authenticated via JWT
   [2026-01-01T19:41:53.588438Z] INFO Viewer Technician joined session de42557c-14cb-4cba-ac0d-0aa15f4c5c7c, sending StartStream
   ```

   Agent's last status was at 19:40:54 (over 3 minutes before current time 19:43:56), indicating the agent on DESKTOP-I66IM5Q may have crashed when processing the StartStream command.

9. Optional Next Step:
   Ask user to check if the guruconnect agent process is still running on DESKTOP-I66IM5Q (the target machine). If crashed, need to examine agent logs or add more debugging to understand why StartStream causes a crash. The issue is likely in the agent's screen capture initialization (DXGI/GDI) or encoder setup when transitioning from idle to streaming mode.

   Last exchange:
   - Assistant: "The agent stopped sending updates 3 minutes ago - it may have crashed when receiving the StartStream command. Is the viewer window still showing white? And can you check on **DESKTOP-I66IM5Q** if the agent is still running? (check for guruconnect process in Task Manager)"
   - User: "Yes, still showing white".
Please continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.


The user confirmed the viewer is still showing white. Based on the summary, the agent appears to have crashed or stopped responding after receiving the StartStream command. I need to investigate what's happening on the agent side.

Let me check the agent's capture/streaming code to understand what might be causing the crash when StartStream is received. The issue is likely in:
1. Screen capture initialization (DXGI/GDI)
2. Encoder setup
3. The transition from idle to streaming mode

I should look at the agent session code to see how it handles the StartStream command.


The agent likely crashed when trying to start screen capture. Let me check the agent's streaming code to understand what happens when StartStream is received.


     1→//! Session management for the agent
     2→//!
     3→//! Handles the lifecycle of a remote session including:
     4→//! - Connection to server
     5→//! - Idle mode (heartbeat only, minimal resources)
     6→//! - Active/streaming mode (capture and send frames)
     7→//! - Input event handling
     8→
     9→#[cfg(windows)]
    10→use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
    11→#[cfg(windows)]
    12→use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
    13→
    14→use crate::capture::{self, Capturer, Display};
    15→use crate::chat::{ChatController, ChatMessage as ChatMsg};
    16→use crate::config::Config;
    17→use crate::encoder::{self, Encoder};
    18→use crate::input::InputController;
    19→
    20→/// Show the debug console window (Windows only)
    21→#[cfg(windows)]
    22→fn show_debug_console() {
    23→    unsafe {
    24→        let hwnd = GetConsoleWindow();
    25→        if hwnd.0 == std::ptr::null_mut() {
    26→            let _ = AllocConsole();
    27→            tracing::info!("Debug console window opened");
    28→        } else {
    29→            let _ = ShowWindow(hwnd, SW_SHOW);
    30→            tracing::info!("Debug console window shown");
    31→        }
    32→    }
    33→}
    34→
    35→#[cfg(not(windows))]
    36→fn show_debug_console() {
    37→    // No-op on non-Windows platforms
    38→}
    39→
    40→use crate::proto::{Message, message, ChatMessage, AgentStatus, Heartbeat, HeartbeatAck};
    41→use crate::transport::WebSocketTransport;
    42→use crate::tray::{TrayController, TrayAction};
    43→use anyhow::Result;
    44→use std::time::{Duration, Instant};
    45→
    46→// Heartbeat interval (30 seconds)
    47→const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
    48→// Status report interval (60 seconds)
    49→const STATUS_INTERVAL: Duration = Duration::from_secs(60);
    50→// Update check interval (1 hour)
    51→const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(3600);
    52→
    53→/// Session manager handles the remote control session
    54→pub struct SessionManager {
    55→    config: Config,
    56→    transport: Option<WebSocketTransport>,
    57→    state: SessionState,
    58→    // Lazy-initialized streaming resources
    59→    capturer: Option<Box<dyn Capturer>>,
    60→    encoder: Option<Box<dyn Encoder>>,
    61→    input: Option<InputController>,
    62→    // Streaming state
    63→    current_viewer_id: Option<String>,
    64→    // System info for status reports
    65→    hostname: String,
    66→    is_elevated: bool,
    67→    start_time: Instant,
    68→}
    69→
    70→#[derive(Debug, Clone, PartialEq)]
    71→enum SessionState {
    72→    Disconnected,
    73→    Connecting,
    74→    Idle,       // Connected but not streaming - minimal resource usage
    75→    Streaming,  // Actively capturing and sending frames
    76→}
    77→
    78→impl SessionManager {
    79→    /// Create a new session manager
    80→    pub fn new(config: Config, is_elevated: bool) -> Self {
    81→        let hostname = config.hostname();
    82→        Self {
    83→            config,
    84→            transport: None,
    85→            state: SessionState::Disconnected,
    86→            capturer: None,
    87→            encoder: None,
    88→            input: None,
    89→            current_viewer_id: None,
    90→            hostname,
    91→            is_elevated,
    92→            start_time: Instant::now(),
    93→        }
    94→    }
    95→
    96→    /// Connect to the server
    97→    pub async fn connect(&mut self) -> Result<()> {
    98→        self.state = SessionState::Connecting;
    99→
   100→        let transport = WebSocketTransport::connect(
   101→            &self.config.server_url,
   102→            &self.config.agent_id,
   103→            &self.config.api_key,
   104→            Some(&self.hostname),
   105→            self.config.support_code.as_deref(),
   106→        ).await?;
   107→
   108→        self.transport = Some(transport);
   109→        self.state = SessionState::Idle;  // Start in idle mode
   110→
   111→        tracing::info!("Connected to server, entering idle mode");
   112→
   113→        Ok(())
   114→    }
   115→
   116→    /// Initialize streaming resources (capturer, encoder, input)
   117→    fn init_streaming(&mut self) -> Result<()> {
   118→        if self.capturer.is_some() {
   119→            return Ok(()); // Already initialized
   120→        }
   121→
   122→        tracing::info!("Initializing streaming resources...");
   123→
   124→        // Get primary display
   125→        let primary_display = capture::primary_display()?;
   126→        tracing::info!("Using display: {} ({}x{})",
   127→            primary_display.name, primary_display.width, primary_display.height);
   128→
   129→        // Create capturer
   130→        let capturer = capture::create_capturer(
   131→            primary_display.clone(),
   132→            self.config.capture.use_dxgi,
   133→            self.config.capture.gdi_fallback,
   134→        )?;
   135→        self.capturer = Some(capturer);
   136→
   137→        // Create encoder
   138→        let encoder = encoder::create_encoder(
   139→            &self.config.encoding.codec,
   140→            self.config.encoding.quality,
   141→        )?;
   142→        self.encoder = Some(encoder);
   143→
   144→        // Create input controller
   145→        let input = InputController::new()?;
   146→        self.input = Some(input);
   147→
   148→        tracing::info!("Streaming resources initialized");
   149→        Ok(())
   150→    }
   151→
   152→    /// Release streaming resources to save CPU/memory when idle
   153→    fn release_streaming(&mut self) {
   154→        if self.capturer.is_some() {
   155→            tracing::info!("Releasing streaming resources");
   156→            self.capturer = None;
   157→            self.encoder = None;
   158→            self.input = None;
   159→            self.current_viewer_id = None;
   160→        }
   161→    }
   162→
   163→    /// Get display count for status reports
   164→    fn get_display_count(&self) -> i32 {
   165→        capture::enumerate_displays().map(|d| d.len() as i32).unwrap_or(1)
   166→    }
   167→
   168→    /// Send agent status to server
   169→    async fn send_status(&mut self) -> Result<()> {
   170→        let status = AgentStatus {
   171→            hostname: self.hostname.clone(),
   172→            os_version: std::env::consts::OS.to_string(),
   173→            is_elevated: self.is_elevated,
   174→            uptime_secs: self.start_time.elapsed().as_secs() as i64,
   175→            display_count: self.get_display_count(),
   176→            is_streaming: self.state == SessionState::Streaming,
   177→            agent_version: crate::build_info::short_version(),
   178→            organization: self.config.company.clone().unwrap_or_default(),
   179→            site: self.config.site.clone().unwrap_or_default(),
   180→            tags: self.config.tags.clone(),
   181→        };
   182→
   183→        let msg = Message {
   184→            payload: Some(message::Payload::AgentStatus(status)),
   185→        };
   186→
   187→        if let Some(transport) = self.transport.as_mut() {
   188→            transport.send(msg).await?;
   189→        }
   190→
   191→        Ok(())
   192→    }
   193→
   194→    /// Send heartbeat to server
   195→    async fn send_heartbeat(&mut self) -> Result<()> {
   196→        let heartbeat = Heartbeat {
   197→            timestamp: chrono::Utc::now().timestamp_millis(),
   198→        };
   199→
   200→        let msg = Message {
   201→            payload: Some(message::Payload::Heartbeat(heartbeat)),
   202→        };
   203→
   204→        if let Some(transport) = self.transport.as_mut() {
   205→            transport.send(msg).await?;
   206→        }
   207→
   208→        Ok(())
   209→    }
   210→
   211→    /// Run the session main loop with tray and chat event processing
   212→    pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> {
   213→        if self.transport.is_none() {
   214→            anyhow::bail!("Not connected");
   215→        }
   216→
   217→        // Send initial status
   218→        self.send_status().await?;
   219→
   220→        // Timing for heartbeat and status
   221→        let mut last_heartbeat = Instant::now();
   222→        let mut last_status = Instant::now();
   223→        let mut last_frame_time = Instant::now();
   224→        let mut last_update_check = Instant::now();
   225→        let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);
   226→
   227→        // Main loop
   228→        loop {
   229→            // Process tray events
   230→            if let Some(t) = tray {
   231→                if let Some(action) = t.process_events() {
   232→                    match action {
   233→                        TrayAction::EndSession => {
   234→                            tracing::info!("User requested session end via tray");
   235→                            return Err(anyhow::anyhow!("USER_EXIT: Session ended by user"));
   236→                        }
   237→                        TrayAction::ShowDetails => {
   238→                            tracing::info!("User requested details (not yet implemented)");
   239→                        }
   240→                        TrayAction::ShowDebugWindow => {
   241→                            show_debug_console();
   242→                        }
   243→                    }
   244→                }
   245→
   246→                if t.exit_requested() {
   247→                    tracing::info!("Exit requested via tray");
   248→                    return Err(anyhow::anyhow!("USER_EXIT: Exit requested by user"));
   249→                }
   250→            }
   251→
   252→            // Process incoming messages
   253→            let messages: Vec<Message> = {
   254→                let transport = self.transport.as_mut().unwrap();
   255→                let mut msgs = Vec::new();
   256→                while let Some(msg) = transport.try_recv()? {
   257→                    msgs.push(msg);
   258→                }
   259→                msgs
   260→            };
   261→
   262→            for msg in messages {
   263→                // Handle chat messages specially
   264→                if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload {
   265→                    if let Some(c) = chat {
   266→                        c.add_message(ChatMsg {
   267→                            id: chat_msg.id.clone(),
   268→                            sender: chat_msg.sender.clone(),
   269→                            content: chat_msg.content.clone(),
   270→                            timestamp: chat_msg.timestamp,
   271→                        });
   272→                    }
   273→                    continue;
   274→                }
   275→
   276→                // Handle control messages that affect state
   277→                if let Some(ref payload) = msg.payload {
   278→                    match payload {
   279→                        message::Payload::StartStream(start) => {
   280→                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);
   281→                            if let Err(e) = self.init_streaming() {
   282→                                tracing::error!("Failed to init streaming: {}", e);
   283→                            } else {
   284→                                self.state = SessionState::Streaming;
   285→                                self.current_viewer_id = Some(start.viewer_id.clone());
   286→                                tracing::info!("Now streaming to viewer {}", start.viewer_id);
   287→                            }
   288→                            continue;
   289→                        }
   290→                        message::Payload::StopStream(stop) => {
   291→                            tracing::info!("StopStream received for viewer: {}", stop.viewer_id);
   292→                            // Only stop if it matches current viewer
   293→                            if self.current_viewer_id.as_ref() == Some(&stop.viewer_id) {
   294→                                self.release_streaming();
   295→                                self.state = SessionState::Idle;
   296→                                tracing::info!("Stopped streaming, returning to idle mode");
   297→                            }
   298→                            continue;
   299→                        }
   300→                        message::Payload::Heartbeat(hb) => {
   301→                            // Respond to server heartbeat with ack
   302→                            let ack = HeartbeatAck {
   303→                                client_timestamp: hb.timestamp,
   304→                                server_timestamp: chrono::Utc::now().timestamp_millis(),
   305→                            };
   306→                            let ack_msg = Message {
   307→                                payload: Some(message::Payload::HeartbeatAck(ack)),
   308→                            };
   309→                            if let Some(transport) = self.transport.as_mut() {
   310→                                let _ = transport.send(ack_msg).await;
   311→                            }
   312→                            continue;
   313→                        }
   314→                        _ => {}
   315→                    }
   316→                }
   317→
   318→                // Handle other messages (input events, disconnect, etc.)
   319→                self.handle_message(msg).await?;
   320→            }
   321→
   322→            // Check for outgoing chat messages
   323→            if let Some(c) = chat {
   324→                if let Some(outgoing) = c.poll_outgoing() {
   325→                    let chat_proto = ChatMessage {
   326→                        id: outgoing.id,
   327→                        sender: "client".to_string(),
   328→                        content: outgoing.content,
   329→                        timestamp: outgoing.timestamp,
   330→                    };
   331→                    let msg = Message {
   332→                        payload: Some(message::Payload::ChatMessage(chat_proto)),
   333→                    };
   334→                    let transport = self.transport.as_mut().unwrap();
   335→                    transport.send(msg).await?;
   336→                }
   337→            }
   338→
   339→            // State-specific behavior
   340→            match self.state {
   341→                SessionState::Idle => {
   342→                    // In idle mode, just send heartbeats and status periodically
   343→                    if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
   344→                        last_heartbeat = Instant::now();
   345→                        if let Err(e) = self.send_heartbeat().await {
   346→                            tracing::warn!("Failed to send heartbeat: {}", e);
   347→                        }
   348→                    }
   349→
   350→                    if last_status.elapsed() >= STATUS_INTERVAL {
   351→                        last_status = Instant::now();
   352→                        if let Err(e) = self.send_status().await {
   353→                            tracing::warn!("Failed to send status: {}", e);
   354→                        }
   355→                    }
   356→
   357→                    // Periodic update check (only for persistent agents, not support sessions)
   358→                    if self.config.support_code.is_none() && last_update_check.elapsed() >= UPDATE_CHECK_INTERVAL {
   359→                        last_update_check = Instant::now();
   360→                        let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
   361→                        match crate::update::check_for_update(&server_url).await {
   362→                            Ok(Some(version_info)) => {
   363→                                tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
   364→                                if let Err(e) = crate::update::perform_update(&version_info).await {
   365→                                    tracing::error!("Auto-update failed: {}", e);
   366→                                }
   367→                            }
   368→                            Ok(None) => {
   369→                                tracing::debug!("No update available");
   370→                            }
   371→                            Err(e) => {
   372→                                tracing::debug!("Update check failed: {}", e);
   373→                            }
   374→                        }
   375→                    }
   376→
   377→                    // Longer sleep in idle mode to reduce CPU usage
   378→                    tokio::time::sleep(Duration::from_millis(100)).await;
   379→                }
   380→                SessionState::Streaming => {
   381→                    // In streaming mode, capture and send frames
   382→                    if last_frame_time.elapsed() >= frame_interval {
   383→                        last_frame_time = Instant::now();
   384→
   385→                        if let (Some(capturer), Some(encoder)) =
   386→                            (self.capturer.as_mut(), self.encoder.as_mut())
   387→                        {
   388→                            if let Ok(Some(frame)) = capturer.capture() {
   389→                                if let Ok(encoded) = encoder.encode(&frame) {
   390→                                    if encoded.size > 0 {
   391→                                        let msg = Message {
   392→                                            payload: Some(message::Payload::VideoFrame(encoded.frame)),
   393→                                        };
   394→                                        let transport = self.transport.as_mut().unwrap();
   395→                                        if let Err(e) = transport.send(msg).await {
   396→                                            tracing::warn!("Failed to send frame: {}", e);
   397→                                        }
   398→                                    }
   399→                                }
   400→                            }
   401→                        }
   402→                    }
   403→
   404→                    // Short sleep in streaming mode
   405→                    tokio::time::sleep(Duration::from_millis(1)).await;
   406→                }
   407→                _ => {
   408→                    // Disconnected or connecting - shouldn't be in main loop
   409→                    tokio::time::sleep(Duration::from_millis(100)).await;
   410→                }
   411→            }
   412→
   413→            // Check if still connected
   414→            if let Some(transport) = self.transport.as_ref() {
   415→                if !transport.is_connected() {
   416→                    tracing::warn!("Connection lost");
   417→                    break;
   418→                }
   419→            } else {
   420→                tracing::warn!("Transport is None");
   421→                break;
   422→            }
   423→        }
   424→
   425→        self.release_streaming();
   426→        self.state = SessionState::Disconnected;
   427→        Ok(())
   428→    }
   429→
   430→    /// Handle incoming message from server
   431→    async fn handle_message(&mut self, msg: Message) -> Result<()> {
   432→        match msg.payload {
   433→            Some(message::Payload::MouseEvent(mouse)) => {
   434→                if let Some(input) = self.input.as_mut() {
   435→                    use crate::proto::MouseEventType;
   436→                    use crate::input::MouseButton;
   437→
   438→                    match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
   439→                        MouseEventType::MouseMove => {
   440→                            input.mouse_move(mouse.x, mouse.y)?;
   441→                        }
   442→                        MouseEventType::MouseDown => {
   443→                            input.mouse_move(mouse.x, mouse.y)?;
   444→                            if let Some(ref buttons) = mouse.buttons {
   445→                                if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
   446→                                if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
   447→                                if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
   448→                            }
   449→                        }
   450→                        MouseEventType::MouseUp => {
   451→                            if let Some(ref buttons) = mouse.buttons {
   452→                                if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
   453→                                if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
   454→                                if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
   455→                            }
   456→                        }
   457→                        MouseEventType::MouseWheel => {
   458→                            input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
   459→                        }
   460→                    }
   461→                }
   462→            }
   463→
   464→            Some(message::Payload::KeyEvent(key)) => {
   465→                if let Some(input) = self.input.as_mut() {
   466→                    input.key_event(key.vk_code as u16, key.down)?;
   467→                }
   468→            }
   469→
   470→            Some(message::Payload::SpecialKey(special)) => {
   471→                if let Some(input) = self.input.as_mut() {
   472→                    use crate::proto::SpecialKey;
   473→                    match SpecialKey::try_from(special.key).ok() {
   474→                        Some(SpecialKey::CtrlAltDel) => {
   475→                            input.send_ctrl_alt_del()?;
   476→                        }
   477→                        _ => {}
   478→                    }
   479→                }
   480→            }
   481→
   482→            Some(message::Payload::AdminCommand(cmd)) => {
   483→                use crate::proto::AdminCommandType;
   484→                tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);
   485→
   486→                match AdminCommandType::try_from(cmd.command).ok() {
   487→                    Some(AdminCommandType::AdminUninstall) => {
   488→                        tracing::warn!("Uninstall command received from server");
   489→                        // Return special error to trigger uninstall in main loop
   490→                        return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
   491→                    }
   492→                    Some(AdminCommandType::AdminRestart) => {
   493→                        tracing::info!("Restart command received from server");
   494→                        // For now, just disconnect - the auto-restart logic will handle it
   495→                        return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
   496→                    }
   497→                    Some(AdminCommandType::AdminUpdate) => {
   498→                        tracing::info!("Update command received from server: {}", cmd.reason);
   499→                        // Trigger update check and perform update if available
   500→                        // The server URL is derived from the config
   501→                        let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
   502→                        match crate::update::check_for_update(&server_url).await {
   503→                            Ok(Some(version_info)) => {
   504→                                tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
   505→                                if let Err(e) = crate::update::perform_update(&version_info).await {
   506→                                    tracing::error!("Update failed: {}", e);
   507→                                }
   508→                                // If we get here, the update failed (perform_update exits on success)
   509→                            }
   510→                            Ok(None) => {
   511→                                tracing::info!("Already running latest version");
   512→                            }
   513→                            Err(e) => {
   514→                                tracing::error!("Failed to check for updates: {}", e);
   515→                            }
   516→                        }
   517→                    }
   518→                    None => {
   519→                        tracing::warn!("Unknown admin command: {}", cmd.command);
   520→                    }
   521→                }
   522→            }
   523→
   524→            Some(message::Payload::Disconnect(disc)) => {
   525→                tracing::info!("Disconnect requested: {}", disc.reason);
   526→                if disc.reason.contains("cancelled") {
   527→                    return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
   528→                }
   529→                if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
   530→                    return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
   531→                }
   532→                return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
   533→            }
   534→
   535→            _ => {
   536→                // Ignore unknown messages
   537→            }
   538→        }
   539→
   540→        Ok(())
   541→    }
   542→}
   543→

<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 the agent
//!
//! Handles the lifecycle of a remote session including:
//! - Connection to server
//! - Idle mode (heartbeat only, minimal resources)
//! - Active/streaming mode (capture and send frames)
//! - Input event handling

#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

use crate::capture::{self, Capturer, Display};
use crate::chat::{ChatController, ChatMessage as ChatMsg};
use crate::config::Config;
use crate::encoder::{self, Encoder};
use crate::input::InputController;

/// Show the debug console window (Windows only)
#[cfg(windows)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0 == std::ptr::null_mut() {
            let _ = AllocConsole();
            tracing::info!("Debug console window opened");
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
            tracing::info!("Debug console window shown");
        }
    }
}

#[cfg(not(windows))]
fn show_debug_console() {
    // No-op on non-Windows platforms
}

use crate::proto::{Message, message, ChatMessage, AgentStatus, Heartbeat, HeartbeatAck};
use crate::transport::WebSocketTransport;
use crate::tray::{TrayController, TrayAction};
use anyhow::Result;
use std::time::{Duration, Instant};

// Heartbeat interval (30 seconds)
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
// Status report interval (60 seconds)
const STATUS_INTERVAL: Duration = Duration::from_secs(60);
// Update check interval (1 hour)
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(3600);

/// Session manager handles the remote control session
pub struct SessionManager {
    config: Config,
    transport: Option<WebSocketTransport>,
    state: SessionState,
    // Lazy-initialized streaming resources
    capturer: Option<Box<dyn Capturer>>,
    encoder: Option<Box<dyn Encoder>>,
    input: Option<InputController>,
    // Streaming state
    current_viewer_id: Option<String>,
    // System info for status reports
    hostname: String,
    is_elevated: bool,
    start_time: Instant,
}

#[derive(Debug, Clone, PartialEq)]
enum SessionState {
    Disconnected,
    Connecting,
    Idle,       // Connected but not streaming - minimal resource usage
    Streaming,  // Actively capturing and sending frames
}

impl SessionManager {
    /// Create a new session manager
    pub fn new(config: Config, is_elevated: bool) -> Self {
        let hostname = config.hostname();
        Self {
            config,
            transport: None,
            state: SessionState::Disconnected,
            capturer: None,
            encoder: None,
            input: None,
            current_viewer_id: None,
            hostname,
            is_elevated,
            start_time: Instant::now(),
        }
    }

    /// Connect to the server
    pub async fn connect(&mut self) -> Result<()> {
        self.state = SessionState::Connecting;

        let transport = WebSocketTransport::connect(
            &self.config.server_url,
            &self.config.agent_id,
            &self.config.api_key,
            Some(&self.hostname),
            self.config.support_code.as_deref(),
        ).await?;

        self.transport = Some(transport);
        self.state = SessionState::Idle;  // Start in idle mode

        tracing::info!("Connected to server, entering idle mode");

        Ok(())
    }

    /// Initialize streaming resources (capturer, encoder, input)
    fn init_streaming(&mut self) -> Result<()> {
        if self.capturer.is_some() {
            return Ok(()); // Already initialized
        }

        tracing::info!("Initializing streaming resources...");

        // Get primary display
        let primary_display = capture::primary_display()?;
        tracing::info!("Using display: {} ({}x{})",
            primary_display.name, primary_display.width, primary_display.height);

        // Create capturer
        let capturer = capture::create_capturer(
            primary_display.clone(),
            self.config.capture.use_dxgi,
            self.config.capture.gdi_fallback,
        )?;
        self.capturer = Some(capturer);

        // Create encoder
        let encoder = encoder::create_encoder(
            &self.config.encoding.codec,
            self.config.encoding.quality,
        )?;
        self.encoder = Some(encoder);

        // Create input controller
        let input = InputController::new()?;
        self.input = Some(input);

        tracing::info!("Streaming resources initialized");
        Ok(())
    }

    /// Release streaming resources to save CPU/memory when idle
    fn release_streaming(&mut self) {
        if self.capturer.is_some() {
            tracing::info!("Releasing streaming resources");
            self.capturer = None;
            self.encoder = None;
            self.input = None;
            self.current_viewer_id = None;
        }
    }

    /// Get display count for status reports
    fn get_display_count(&self) -> i32 {
        capture::enumerate_displays().map(|d| d.len() as i32).unwrap_or(1)
    }

    /// Send agent status to server
    async fn send_status(&mut self) -> Result<()> {
        let status = AgentStatus {
            hostname: self.hostname.clone(),
            os_version: std::env::consts::OS.to_string(),
            is_elevated: self.is_elevated,
            uptime_secs: self.start_time.elapsed().as_secs() as i64,
            display_count: self.get_display_count(),
            is_streaming: self.state == SessionState::Streaming,
            agent_version: crate::build_info::short_version(),
            organization: self.config.company.clone().unwrap_or_default(),
            site: self.config.site.clone().unwrap_or_default(),
            tags: self.config.tags.clone(),
        };

        let msg = Message {
            payload: Some(message::Payload::AgentStatus(status)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Send heartbeat to server
    async fn send_heartbeat(&mut self) -> Result<()> {
        let heartbeat = Heartbeat {
            timestamp: chrono::Utc::now().timestamp_millis(),
        };

        let msg = Message {
            payload: Some(message::Payload::Heartbeat(heartbeat)),
        };

        if let Some(transport) = self.transport.as_mut() {
            transport.send(msg).await?;
        }

        Ok(())
    }

    /// Run the session main loop with tray and chat event processing
    pub async fn run_with_tray(&mut self, tray: Option<&TrayController>, chat: Option<&ChatController>) -> Result<()> {
        if self.transport.is_none() {
            anyhow::bail!("Not connected");
        }

        // Send initial status
        self.send_status().await?;

        // Timing for heartbeat and status
        let mut last_heartbeat = Instant::now();
        let mut last_status = Instant::now();
        let mut last_frame_time = Instant::now();
        let mut last_update_check = Instant::now();
        let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);

        // Main loop
        loop {
            // Process tray events
            if let Some(t) = tray {
                if let Some(action) = t.process_events() {
                    match action {
                        TrayAction::EndSession => {
                            tracing::info!("User requested session end via tray");
                            return Err(anyhow::anyhow!("USER_EXIT: Session ended by user"));
                        }
                        TrayAction::ShowDetails => {
                            tracing::info!("User requested details (not yet implemented)");
                        }
                        TrayAction::ShowDebugWindow => {
                            show_debug_console();
                        }
                    }
                }

                if t.exit_requested() {
                    tracing::info!("Exit requested via tray");
                    return Err(anyhow::anyhow!("USER_EXIT: Exit requested by user"));
                }
            }

            // Process incoming messages
            let messages: Vec<Message> = {
                let transport = self.transport.as_mut().unwrap();
                let mut msgs = Vec::new();
                while let Some(msg) = transport.try_recv()? {
                    msgs.push(msg);
                }
                msgs
            };

            for msg in messages {
                // Handle chat messages specially
                if let Some(message::Payload::ChatMessage(chat_msg)) = &msg.payload {
                    if let Some(c) = chat {
                        c.add_message(ChatMsg {
                            id: chat_msg.id.clone(),
                            sender: chat_msg.sender.clone(),
                            content: chat_msg.content.clone(),
                            timestamp: chat_msg.timestamp,
                        });
                    }
                    continue;
                }

                // Handle control messages that affect state
                if let Some(ref payload) = msg.payload {
                    match payload {
                        message::Payload::StartStream(start) => {
                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);
                            if let Err(e) = self.init_streaming() {
                                tracing::error!("Failed to init streaming: {}", e);
                            } else {
                                self.state = SessionState::Streaming;
                                self.current_viewer_id = Some(start.viewer_id.clone());
                                tracing::info!("Now streaming to viewer {}", start.viewer_id);
                            }
                            continue;
                        }
                        message::Payload::StopStream(stop) => {
                            tracing::info!("StopStream received for viewer: {}", stop.viewer_id);
                            // Only stop if it matches current viewer
                            if self.current_viewer_id.as_ref() == Some(&stop.viewer_id) {
                                self.release_streaming();
                                self.state = SessionState::Idle;
                                tracing::info!("Stopped streaming, returning to idle mode");
                            }
                            continue;
                        }
                        message::Payload::Heartbeat(hb) => {
                            // Respond to server heartbeat with ack
                            let ack = HeartbeatAck {
                                client_timestamp: hb.timestamp,
                                server_timestamp: chrono::Utc::now().timestamp_millis(),
                            };
                            let ack_msg = Message {
                                payload: Some(message::Payload::HeartbeatAck(ack)),
                            };
                            if let Some(transport) = self.transport.as_mut() {
                                let _ = transport.send(ack_msg).await;
                            }
                            continue;
                        }
                        _ => {}
                    }
                }

                // Handle other messages (input events, disconnect, etc.)
                self.handle_message(msg).await?;
            }

            // Check for outgoing chat messages
            if let Some(c) = chat {
                if let Some(outgoing) = c.poll_outgoing() {
                    let chat_proto = ChatMessage {
                        id: outgoing.id,
                        sender: "client".to_string(),
                        content: outgoing.content,
                        timestamp: outgoing.timestamp,
                    };
                    let msg = Message {
                        payload: Some(message::Payload::ChatMessage(chat_proto)),
                    };
                    let transport = self.transport.as_mut().unwrap();
                    transport.send(msg).await?;
                }
            }

            // State-specific behavior
            match self.state {
                SessionState::Idle => {
                    // In idle mode, just send heartbeats and status periodically
                    if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
                        last_heartbeat = Instant::now();
                        if let Err(e) = self.send_heartbeat().await {
                            tracing::warn!("Failed to send heartbeat: {}", e);
                        }
                    }

                    if last_status.elapsed() >= STATUS_INTERVAL {
                        last_status = Instant::now();
                        if let Err(e) = self.send_status().await {
                            tracing::warn!("Failed to send status: {}", e);
                        }
                    }

                    // Periodic update check (only for persistent agents, not support sessions)
                    if self.config.support_code.is_none() && last_update_check.elapsed() >= UPDATE_CHECK_INTERVAL {
                        last_update_check = Instant::now();
                        let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
                        match crate::update::check_for_update(&server_url).await {
                            Ok(Some(version_info)) => {
                                tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
                                if let Err(e) = crate::update::perform_update(&version_info).await {
                                    tracing::error!("Auto-update failed: {}", e);
                                }
                            }
                            Ok(None) => {
                                tracing::debug!("No update available");
                            }
                            Err(e) => {
                                tracing::debug!("Update check failed: {}", e);
                            }
                        }
                    }

                    // Longer sleep in idle mode to reduce CPU usage
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
                SessionState::Streaming => {
                    // In streaming mode, capture and send frames
                    if last_frame_time.elapsed() >= frame_interval {
                        last_frame_time = Instant::now();

                        if let (Some(capturer), Some(encoder)) =
                            (self.capturer.as_mut(), self.encoder.as_mut())
                        {
                            if let Ok(Some(frame)) = capturer.capture() {
                                if let Ok(encoded) = encoder.encode(&frame) {
                                    if encoded.size > 0 {
                                        let msg = Message {
                                            payload: Some(message::Payload::VideoFrame(encoded.frame)),
                                        };
                                        let transport = self.transport.as_mut().unwrap();
                                        if let Err(e) = transport.send(msg).await {
                                            tracing::warn!("Failed to send frame: {}", e);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // Short sleep in streaming mode
                    tokio::time::sleep(Duration::from_millis(1)).await;
                }
                _ => {
                    // Disconnected or connecting - shouldn't be in main loop
                    tokio::time::sleep(Duration::from_millis(100)).await;
                }
            }

            // Check if still connected
            if let Some(transport) = self.transport.as_ref() {
                if !transport.is_connected() {
                    tracing::warn!("Connection lost");
                    break;
                }
            } else {
                tracing::warn!("Transport is None");
                break;
            }
        }

        self.release_streaming();
        self.state = SessionState::Disconnected;
        Ok(())
    }

    /// Handle incoming message from server
    async fn handle_message(&mut self, msg: Message) -> Result<()> {
        match msg.payload {
            Some(message::Payload::MouseEvent(mouse)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::proto::MouseEventType;
                    use crate::input::MouseButton;

                    match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
                        MouseEventType::MouseMove => {
                            input.mouse_move(mouse.x, mouse.y)?;
                        }
                        MouseEventType::MouseDown => {
                            input.mouse_move(mouse.x, mouse.y)?;
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
                                if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
                                if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
                            }
                        }
                        MouseEventType::MouseUp => {
                            if let Some(ref buttons) = mouse.buttons {
                                if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
                                if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
                                if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
                            }
                        }
                        MouseEventType::MouseWheel => {
                            input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
                        }
                    }
                }
            }

            Some(message::Payload::KeyEvent(key)) => {
                if let Some(input) = self.input.as_mut() {
                    input.key_event(key.vk_code as u16, key.down)?;
                }
            }

            Some(message::Payload::SpecialKey(special)) => {
                if let Some(input) = self.input.as_mut() {
                    use crate::proto::SpecialKey;
                    match SpecialKey::try_from(special.key).ok() {
                        Some(SpecialKey::CtrlAltDel) => {
                            input.send_ctrl_alt_del()?;
                        }
                        _ => {}
                    }
                }
            }

            Some(message::Payload::AdminCommand(cmd)) => {
                use crate::proto::AdminCommandType;
                tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);

                match AdminCommandType::try_from(cmd.command).ok() {
                    Some(AdminCommandType::AdminUninstall) => {
                        tracing::warn!("Uninstall command received from server");
                        // Return special error to trigger uninstall in main loop
                        return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminRestart) => {
                        tracing::info!("Restart command received from server");
                        // For now, just disconnect - the auto-restart logic will handle it
                        return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
                    }
                    Some(AdminCommandType::AdminUpdate) => {
                        tracing::info!("Update command received from server: {}", cmd.reason);
                        // Trigger update check and perform update if available
                        // The server URL is derived from the config
                        let server_url = self.config.server_url.replace("/ws/agent", "").replace("wss://", "https://").replace("ws://", "http://");
                        match crate::update::check_for_update(&server_url).await {
                            Ok(Some(version_info)) => {
                                tracing::info!("Update available: {} -> {}", crate::build_info::VERSION, version_info.latest_version);
                                if let Err(e) = crate::update::perform_update(&version_info).await {
                                    tracing::error!("Update failed: {}", e);
                                }
                                // If we get here, the update failed (perform_update exits on success)
                            }
                            Ok(None) => {
                                tracing::info!("Already running latest version");
                            }
                            Err(e) => {
                                tracing::error!("Failed to check for updates: {}", e);
                            }
                        }
                    }
                    None => {
                        tracing::warn!("Unknown admin command: {}", cmd.command);
                    }
                }
            }

            Some(message::Payload::Disconnect(disc)) => {
                tracing::info!("Disconnect requested: {}", disc.reason);
                if disc.reason.contains("cancelled") {
                    return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
                }
                if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
                    return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
                }
                return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
            }

            _ => {
                // Ignore unknown messages
            }
        }

        Ok(())
    }
}


C:\Users\MikeSwanson\Claude\guru-connect\agent\src\session\mod.rs:279:                        message::Payload::StartStream(start) => {
C:\Users\MikeSwanson\Claude\guru-connect\agent\src\session\mod.rs:280:                            tracing::info!("StartStream received from viewer: {}", start.viewer_id);


I can see the agent code. When StartStream is received (line 279-288), it calls `self.init_streaming()` which in turn:
1. Gets primary display via `capture::primary_display()`
2. Creates a capturer
3. Creates an encoder
4. Creates an input controller

The crash is likely happening in `init_streaming()` during one of these steps. Let me check the capture module to see what might be failing - especially since the Geovision machine might have multiple displays or unusual graphics configuration.

Let me check the capture module for potential issues:


The code shows that when StartStream is received, `init_streaming()` is called which initializes capture, encoder, and input. Let me check the capture module - the crash is likely happening there with DXGI or the display enumeration:


     1→//! Screen capture module
     2→//!
     3→//! Provides DXGI Desktop Duplication for high-performance screen capture on Windows 8+,
     4→//! with GDI fallback for legacy systems or edge cases.
     5→
     6→#[cfg(windows)]
     7→mod dxgi;
     8→#[cfg(windows)]
     9→mod gdi;
    10→mod display;
    11→
    12→pub use display::{Display, DisplayInfo};
    13→
    14→use anyhow::Result;
    15→use std::time::Instant;
    16→
    17→/// Captured frame data
    18→#[derive(Debug)]
    19→pub struct CapturedFrame {
    20→    /// Frame width in pixels
    21→    pub width: u32,
    22→
    23→    /// Frame height in pixels
    24→    pub height: u32,
    25→
    26→    /// Raw BGRA pixel data (4 bytes per pixel)
    27→    pub data: Vec<u8>,
    28→
    29→    /// Timestamp when frame was captured
    30→    pub timestamp: Instant,
    31→
    32→    /// Display ID this frame is from
    33→    pub display_id: u32,
    34→
    35→    /// Regions that changed since last frame (if available)
    36→    pub dirty_rects: Option<Vec<DirtyRect>>,
    37→}
    38→
    39→/// Rectangular region that changed
    40→#[derive(Debug, Clone, Copy)]
    41→pub struct DirtyRect {
    42→    pub x: u32,
    43→    pub y: u32,
    44→    pub width: u32,
    45→    pub height: u32,
    46→}
    47→
    48→/// Screen capturer trait
    49→pub trait Capturer: Send {
    50→    /// Capture the next frame
    51→    ///
    52→    /// Returns None if no new frame is available (screen unchanged)
    53→    fn capture(&mut self) -> Result<Option<CapturedFrame>>;
    54→
    55→    /// Get the current display info
    56→    fn display(&self) -> &Display;
    57→
    58→    /// Check if capturer is still valid (display may have changed)
    59→    fn is_valid(&self) -> bool;
    60→}
    61→
    62→/// Create a capturer for the specified display
    63→#[cfg(windows)]
    64→pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
    65→    if use_dxgi {
    66→        match dxgi::DxgiCapturer::new(display.clone()) {
    67→            Ok(capturer) => {
    68→                tracing::info!("Using DXGI Desktop Duplication for capture");
    69→                return Ok(Box::new(capturer));
    70→            }
    71→            Err(e) => {
    72→                tracing::warn!("DXGI capture failed: {}, trying fallback", e);
    73→                if !gdi_fallback {
    74→                    return Err(e);
    75→                }
    76→            }
    77→        }
    78→    }
    79→
    80→    // GDI fallback
    81→    tracing::info!("Using GDI for capture");
    82→    Ok(Box::new(gdi::GdiCapturer::new(display)?))
    83→}
    84→
    85→#[cfg(not(windows))]
    86→pub fn create_capturer(_display: Display, _use_dxgi: bool, _gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
    87→    anyhow::bail!("Screen capture only supported on Windows")
    88→}
    89→
    90→/// Get all available displays
    91→pub fn enumerate_displays() -> Result<Vec<Display>> {
    92→    display::enumerate_displays()
    93→}
    94→
    95→/// Get the primary display
    96→pub fn primary_display() -> Result<Display> {
    97→    let displays = enumerate_displays()?;
    98→    displays
    99→        .into_iter()
   100→        .find(|d| d.is_primary)
   101→        .ok_or_else(|| anyhow::anyhow!("No primary display found"))
   102→}
   103→

<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>

//! Screen capture module
//!
//! Provides DXGI Desktop Duplication for high-performance screen capture on Windows 8+,
//! with GDI fallback for legacy systems or edge cases.

#[cfg(windows)]
mod dxgi;
#[cfg(windows)]
mod gdi;
mod display;

pub use display::{Display, DisplayInfo};

use anyhow::Result;
use std::time::Instant;

/// Captured frame data
#[derive(Debug)]
pub struct CapturedFrame {
    /// Frame width in pixels
    pub width: u32,

    /// Frame height in pixels
    pub height: u32,

    /// Raw BGRA pixel data (4 bytes per pixel)
    pub data: Vec<u8>,

    /// Timestamp when frame was captured
    pub timestamp: Instant,

    /// Display ID this frame is from
    pub display_id: u32,

    /// Regions that changed since last frame (if available)
    pub dirty_rects: Option<Vec<DirtyRect>>,
}

/// Rectangular region that changed
#[derive(Debug, Clone, Copy)]
pub struct DirtyRect {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

/// Screen capturer trait
pub trait Capturer: Send {
    /// Capture the next frame
    ///
    /// Returns None if no new frame is available (screen unchanged)
    fn capture(&mut self) -> Result<Option<CapturedFrame>>;

    /// Get the current display info
    fn display(&self) -> &Display;

    /// Check if capturer is still valid (display may have changed)
    fn is_valid(&self) -> bool;
}

/// Create a capturer for the specified display
#[cfg(windows)]
pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
    if use_dxgi {
        match dxgi::DxgiCapturer::new(display.clone()) {
            Ok(capturer) => {
                tracing::info!("Using DXGI Desktop Duplication for capture");
                return Ok(Box::new(capturer));
            }
            Err(e) => {
                tracing::warn!("DXGI capture failed: {}, trying fallback", e);
                if !gdi_fallback {
                    return Err(e);
                }
            }
        }
    }

    // GDI fallback
    tracing::info!("Using GDI for capture");
    Ok(Box::new(gdi::GdiCapturer::new(display)?))
}

#[cfg(not(windows))]
pub fn create_capturer(_display: Display, _use_dxgi: bool, _gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
    anyhow::bail!("Screen capture only supported on Windows")
}

/// Get all available displays
pub fn enumerate_displays() -> Result<Vec<Display>> {
    display::enumerate_displays()
}

/// Get the primary display
pub fn primary_display() -> Result<Display> {
    let displays = enumerate_displays()?;
    displays
        .into_iter()
        .find(|d| d.is_primary)
        .ok_or_else(|| anyhow::anyhow!("No primary display found"))
}


The crash likely happens in DXGI capture initialization. Let me check the DXGI module - Geovision systems often have specialized graphics or multiple monitor outputs that could cause issues with DXGI Desktop Duplication.


Let me check the DXGI capturer - Geovision systems often have multiple displays or specialized graphics that can cause issues with Desktop Duplication:


     1→//! DXGI Desktop Duplication screen capture
     2→//!
     3→//! Uses the Windows Desktop Duplication API (available on Windows 8+) for
     4→//! high-performance, low-latency screen capture with hardware acceleration.
     5→//!
     6→//! Reference: RustDesk's scrap library implementation
     7→
     8→use super::{CapturedFrame, Capturer, DirtyRect, Display};
     9→use anyhow::{Context, Result};
    10→use std::ptr;
    11→use std::time::Instant;
    12→
    13→use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
    14→use windows::Win32::Graphics::Direct3D11::{
    15→    D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
    16→    D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
    17→    D3D11_USAGE_STAGING, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ,
    18→};
    19→use windows::Win32::Graphics::Dxgi::{
    20→    CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IDXGIOutput1,
    21→    IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
    22→    DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
    23→    DXGI_RESOURCE_PRIORITY_MAXIMUM,
    24→};
    25→use windows::core::Interface;
    26→
    27→/// DXGI Desktop Duplication capturer
    28→pub struct DxgiCapturer {
    29→    display: Display,
    30→    device: ID3D11Device,
    31→    context: ID3D11DeviceContext,
    32→    duplication: IDXGIOutputDuplication,
    33→    staging_texture: Option<ID3D11Texture2D>,
    34→    width: u32,
    35→    height: u32,
    36→    last_frame: Option<Vec<u8>>,
    37→}
    38→
    39→impl DxgiCapturer {
    40→    /// Create a new DXGI capturer for the specified display
    41→    pub fn new(display: Display) -> Result<Self> {
    42→        let (device, context, duplication, desc) = Self::create_duplication(&display)?;
    43→
    44→        Ok(Self {
    45→            display,
    46→            device,
    47→            context,
    48→            duplication,
    49→            staging_texture: None,
    50→            width: desc.ModeDesc.Width,
    51→            height: desc.ModeDesc.Height,
    52→            last_frame: None,
    53→        })
    54→    }
    55→
    56→    /// Create D3D device and output duplication
    57→    fn create_duplication(
    58→        target_display: &Display,
    59→    ) -> Result<(ID3D11Device, ID3D11DeviceContext, IDXGIOutputDuplication, DXGI_OUTDUPL_DESC)> {
    60→        unsafe {
    61→            // Create DXGI factory
    62→            let factory: IDXGIFactory1 = CreateDXGIFactory1()
    63→                .context("Failed to create DXGI factory")?;
    64→
    65→            // Find the adapter and output for this display
    66→            let (adapter, output) = Self::find_adapter_output(&factory, target_display)?;
    67→
    68→            // Create D3D11 device
    69→            let mut device: Option<ID3D11Device> = None;
    70→            let mut context: Option<ID3D11DeviceContext> = None;
    71→
    72→            D3D11CreateDevice(
    73→                &adapter,
    74→                D3D_DRIVER_TYPE_UNKNOWN,
    75→                None,
    76→                Default::default(),
    77→                None,
    78→                D3D11_SDK_VERSION,
    79→                Some(&mut device),
    80→                None,
    81→                Some(&mut context),
    82→            )
    83→            .context("Failed to create D3D11 device")?;
    84→
    85→            let device = device.context("D3D11 device is None")?;
    86→            let context = context.context("D3D11 context is None")?;
    87→
    88→            // Get IDXGIOutput1 interface
    89→            let output1: IDXGIOutput1 = output.cast()
    90→                .context("Failed to get IDXGIOutput1 interface")?;
    91→
    92→            // Create output duplication
    93→            let duplication = output1.DuplicateOutput(&device)
    94→                .context("Failed to create output duplication")?;
    95→
    96→            // Get duplication description
    97→            let desc = duplication.GetDesc();
    98→
    99→            tracing::info!(
   100→                "Created DXGI duplication: {}x{}, display: {}",
   101→                desc.ModeDesc.Width,
   102→                desc.ModeDesc.Height,
   103→                target_display.name
   104→            );
   105→
   106→            Ok((device, context, duplication, desc))
   107→        }
   108→    }
   109→
   110→    /// Find the adapter and output for the specified display
   111→    fn find_adapter_output(
   112→        factory: &IDXGIFactory1,
   113→        display: &Display,
   114→    ) -> Result<(IDXGIAdapter1, IDXGIOutput)> {
   115→        unsafe {
   116→            let mut adapter_idx = 0u32;
   117→
   118→            loop {
   119→                // Enumerate adapters
   120→                let adapter: IDXGIAdapter1 = match factory.EnumAdapters1(adapter_idx) {
   121→                    Ok(a) => a,
   122→                    Err(_) => break,
   123→                };
   124→
   125→                let mut output_idx = 0u32;
   126→
   127→                loop {
   128→                    // Enumerate outputs for this adapter
   129→                    let output: IDXGIOutput = match adapter.EnumOutputs(output_idx) {
   130→                        Ok(o) => o,
   131→                        Err(_) => break,
   132→                    };
   133→
   134→                    // Check if this is the display we want
   135→                    let desc = output.GetDesc()?;
   136→
   137→                    let name = String::from_utf16_lossy(
   138→                        &desc.DeviceName[..desc.DeviceName.iter().position(|&c| c == 0).unwrap_or(desc.DeviceName.len())]
   139→                    );
   140→
   141→                    if name == display.name || desc.Monitor.0 as isize == display.handle {
   142→                        return Ok((adapter, output));
   143→                    }
   144→
   145→                    output_idx += 1;
   146→                }
   147→
   148→                adapter_idx += 1;
   149→            }
   150→
   151→            // If we didn't find the specific display, use the first one
   152→            let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0)
   153→                .context("No adapters found")?;
   154→            let output: IDXGIOutput = adapter.EnumOutputs(0)
   155→                .context("No outputs found")?;
   156→
   157→            Ok((adapter, output))
   158→        }
   159→    }
   160→
   161→    /// Create or get the staging texture for CPU access
   162→    fn get_staging_texture(&mut self, src_texture: &ID3D11Texture2D) -> Result<&ID3D11Texture2D> {
   163→        if self.staging_texture.is_none() {
   164→            unsafe {
   165→                let mut desc = D3D11_TEXTURE2D_DESC::default();
   166→                src_texture.GetDesc(&mut desc);
   167→
   168→                desc.Usage = D3D11_USAGE_STAGING;
   169→                desc.BindFlags = Default::default();
   170→                desc.CPUAccessFlags = 0x20000; // D3D11_CPU_ACCESS_READ
   171→                desc.MiscFlags = Default::default();
   172→
   173→                let mut staging: Option<ID3D11Texture2D> = None;
   174→                self.device.CreateTexture2D(&desc, None, Some(&mut staging))
   175→                    .context("Failed to create staging texture")?;
   176→
   177→                let staging = staging.context("Staging texture is None")?;
   178→
   179→                // Set high priority
   180→                let resource: IDXGIResource = staging.cast()?;
   181→                resource.SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM)?;
   182→
   183→                self.staging_texture = Some(staging);
   184→            }
   185→        }
   186→
   187→        Ok(self.staging_texture.as_ref().unwrap())
   188→    }
   189→
   190→    /// Acquire the next frame from the desktop
   191→    fn acquire_frame(&mut self, timeout_ms: u32) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
   192→        unsafe {
   193→            let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
   194→            let mut desktop_resource: Option<IDXGIResource> = None;
   195→
   196→            let result = self.duplication.AcquireNextFrame(
   197→                timeout_ms,
   198→                &mut frame_info,
   199→                &mut desktop_resource,
   200→            );
   201→
   202→            match result {
   203→                Ok(_) => {
   204→                    let resource = desktop_resource.context("Desktop resource is None")?;
   205→
   206→                    // Check if there's actually a new frame
   207→                    if frame_info.LastPresentTime == 0 {
   208→                        self.duplication.ReleaseFrame().ok();
   209→                        return Ok(None);
   210→                    }
   211→
   212→                    let texture: ID3D11Texture2D = resource.cast()
   213→                        .context("Failed to cast to ID3D11Texture2D")?;
   214→
   215→                    Ok(Some((texture, frame_info)))
   216→                }
   217→                Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
   218→                    // No new frame available
   219→                    Ok(None)
   220→                }
   221→                Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
   222→                    // Desktop duplication was invalidated, need to recreate
   223→                    tracing::warn!("Desktop duplication access lost, will need to recreate");
   224→                    Err(anyhow::anyhow!("Access lost"))
   225→                }
   226→                Err(e) => {
   227→                    Err(e).context("Failed to acquire frame")
   228→                }
   229→            }
   230→        }
   231→    }
   232→
   233→    /// Copy frame data to CPU-accessible memory
   234→    fn copy_frame_data(&mut self, texture: &ID3D11Texture2D) -> Result<Vec<u8>> {
   235→        unsafe {
   236→            // Get or create staging texture
   237→            let staging = self.get_staging_texture(texture)?.clone();
   238→
   239→            // Copy from GPU texture to staging texture
   240→            self.context.CopyResource(&staging, texture);
   241→
   242→            // Map the staging texture for CPU read
   243→            let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
   244→            self.context
   245→                .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
   246→                .context("Failed to map staging texture")?;
   247→
   248→            // Copy pixel data
   249→            let src_pitch = mapped.RowPitch as usize;
   250→            let dst_pitch = (self.width * 4) as usize;
   251→            let height = self.height as usize;
   252→
   253→            let mut data = vec![0u8; dst_pitch * height];
   254→
   255→            let src_ptr = mapped.pData as *const u8;
   256→            for y in 0..height {
   257→                let src_row = src_ptr.add(y * src_pitch);
   258→                let dst_row = data.as_mut_ptr().add(y * dst_pitch);
   259→                ptr::copy_nonoverlapping(src_row, dst_row, dst_pitch);
   260→            }
   261→
   262→            // Unmap
   263→            self.context.Unmap(&staging, 0);
   264→
   265→            Ok(data)
   266→        }
   267→    }
   268→
   269→    /// Extract dirty rectangles from frame info
   270→    fn extract_dirty_rects(&self, _frame_info: &DXGI_OUTDUPL_FRAME_INFO) -> Option<Vec<DirtyRect>> {
   271→        // TODO: Implement dirty rectangle extraction using
   272→        // IDXGIOutputDuplication::GetFrameDirtyRects and GetFrameMoveRects
   273→        // For now, return None to indicate full frame update
   274→        None
   275→    }
   276→}
   277→
   278→impl Capturer for DxgiCapturer {
   279→    fn capture(&mut self) -> Result<Option<CapturedFrame>> {
   280→        // Try to acquire a frame with 100ms timeout
   281→        let frame_result = self.acquire_frame(100)?;
   282→
   283→        let (texture, frame_info) = match frame_result {
   284→            Some((t, f)) => (t, f),
   285→            None => return Ok(None), // No new frame
   286→        };
   287→
   288→        // Copy frame data to CPU memory
   289→        let data = self.copy_frame_data(&texture)?;
   290→
   291→        // Release the frame
   292→        unsafe {
   293→            self.duplication.ReleaseFrame().ok();
   294→        }
   295→
   296→        // Extract dirty rectangles if available
   297→        let dirty_rects = self.extract_dirty_rects(&frame_info);
   298→
   299→        Ok(Some(CapturedFrame {
   300→            width: self.width,
   301→            height: self.height,
   302→            data,
   303→            timestamp: Instant::now(),
   304→            display_id: self.display.id,
   305→            dirty_rects,
   306→        }))
   307→    }
   308→
   309→    fn display(&self) -> &Display {
   310→        &self.display
   311→    }
   312→
   313→    fn is_valid(&self) -> bool {
   314→        // Could check if duplication is still valid
   315→        true
   316→    }
   317→}
   318→
   319→impl Drop for DxgiCapturer {
   320→    fn drop(&mut self) {
   321→        // Release any held frame
   322→        unsafe {
   323→            self.duplication.ReleaseFrame().ok();
   324→        }
   325→    }
   326→}
   327→

<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>

//! DXGI Desktop Duplication screen capture
//!
//! Uses the Windows Desktop Duplication API (available on Windows 8+) for
//! high-performance, low-latency screen capture with hardware acceleration.
//!
//! Reference: RustDesk's scrap library implementation

use super::{CapturedFrame, Capturer, DirtyRect, Display};
use anyhow::{Context, Result};
use std::ptr;
use std::time::Instant;

use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
use windows::Win32::Graphics::Direct3D11::{
    D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
    D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
    D3D11_USAGE_STAGING, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ,
};
use windows::Win32::Graphics::Dxgi::{
    CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IDXGIOutput1,
    IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
    DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
    DXGI_RESOURCE_PRIORITY_MAXIMUM,
};
use windows::core::Interface;

/// DXGI Desktop Duplication capturer
pub struct DxgiCapturer {
    display: Display,
    device: ID3D11Device,
    context: ID3D11DeviceContext,
    duplication: IDXGIOutputDuplication,
    staging_texture: Option<ID3D11Texture2D>,
    width: u32,
    height: u32,
    last_frame: Option<Vec<u8>>,
}

impl DxgiCapturer {
    /// Create a new DXGI capturer for the specified display
    pub fn new(display: Display) -> Result<Self> {
        let (device, context, duplication, desc) = Self::create_duplication(&display)?;

        Ok(Self {
            display,
            device,
            context,
            duplication,
            staging_texture: None,
            width: desc.ModeDesc.Width,
            height: desc.ModeDesc.Height,
            last_frame: None,
        })
    }

    /// Create D3D device and output duplication
    fn create_duplication(
        target_display: &Display,
    ) -> Result<(ID3D11Device, ID3D11DeviceContext, IDXGIOutputDuplication, DXGI_OUTDUPL_DESC)> {
        unsafe {
            // Create DXGI factory
            let factory: IDXGIFactory1 = CreateDXGIFactory1()
                .context("Failed to create DXGI factory")?;

            // Find the adapter and output for this display
            let (adapter, output) = Self::find_adapter_output(&factory, target_display)?;

            // Create D3D11 device
            let mut device: Option<ID3D11Device> = None;
            let mut context: Option<ID3D11DeviceContext> = None;

            D3D11CreateDevice(
                &adapter,
                D3D_DRIVER_TYPE_UNKNOWN,
                None,
                Default::default(),
                None,
                D3D11_SDK_VERSION,
                Some(&mut device),
                None,
                Some(&mut context),
            )
            .context("Failed to create D3D11 device")?;

            let device = device.context("D3D11 device is None")?;
            let context = context.context("D3D11 context is None")?;

            // Get IDXGIOutput1 interface
            let output1: IDXGIOutput1 = output.cast()
                .context("Failed to get IDXGIOutput1 interface")?;

            // Create output duplication
            let duplication = output1.DuplicateOutput(&device)
                .context("Failed to create output duplication")?;

            // Get duplication description
            let desc = duplication.GetDesc();

            tracing::info!(
                "Created DXGI duplication: {}x{}, display: {}",
                desc.ModeDesc.Width,
                desc.ModeDesc.Height,
                target_display.name
            );

            Ok((device, context, duplication, desc))
        }
    }

    /// Find the adapter and output for the specified display
    fn find_adapter_output(
        factory: &IDXGIFactory1,
        display: &Display,
    ) -> Result<(IDXGIAdapter1, IDXGIOutput)> {
        unsafe {
            let mut adapter_idx = 0u32;

            loop {
                // Enumerate adapters
                let adapter: IDXGIAdapter1 = match factory.EnumAdapters1(adapter_idx) {
                    Ok(a) => a,
                    Err(_) => break,
                };

                let mut output_idx = 0u32;

                loop {
                    // Enumerate outputs for this adapter
                    let output: IDXGIOutput = match adapter.EnumOutputs(output_idx) {
                        Ok(o) => o,
                        Err(_) => break,
                    };

                    // Check if this is the display we want
                    let desc = output.GetDesc()?;

                    let name = String::from_utf16_lossy(
                        &desc.DeviceName[..desc.DeviceName.iter().position(|&c| c == 0).unwrap_or(desc.DeviceName.len())]
                    );

                    if name == display.name || desc.Monitor.0 as isize == display.handle {
                        return Ok((adapter, output));
                    }

                    output_idx += 1;
                }

                adapter_idx += 1;
            }

            // If we didn't find the specific display, use the first one
            let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0)
                .context("No adapters found")?;
            let output: IDXGIOutput = adapter.EnumOutputs(0)
                .context("No outputs found")?;

            Ok((adapter, output))
        }
    }

    /// Create or get the staging texture for CPU access
    fn get_staging_texture(&mut self, src_texture: &ID3D11Texture2D) -> Result<&ID3D11Texture2D> {
        if self.staging_texture.is_none() {
            unsafe {
                let mut desc = D3D11_TEXTURE2D_DESC::default();
                src_texture.GetDesc(&mut desc);

                desc.Usage = D3D11_USAGE_STAGING;
                desc.BindFlags = Default::default();
                desc.CPUAccessFlags = 0x20000; // D3D11_CPU_ACCESS_READ
                desc.MiscFlags = Default::default();

                let mut staging: Option<ID3D11Texture2D> = None;
                self.device.CreateTexture2D(&desc, None, Some(&mut staging))
                    .context("Failed to create staging texture")?;

                let staging = staging.context("Staging texture is None")?;

                // Set high priority
                let resource: IDXGIResource = staging.cast()?;
                resource.SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM)?;

                self.staging_texture = Some(staging);
            }
        }

        Ok(self.staging_texture.as_ref().unwrap())
    }

    /// Acquire the next frame from the desktop
    fn acquire_frame(&mut self, timeout_ms: u32) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
        unsafe {
            let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
            let mut desktop_resource: Option<IDXGIResource> = None;

            let result = self.duplication.AcquireNextFrame(
                timeout_ms,
                &mut frame_info,
                &mut desktop_resource,
            );

            match result {
                Ok(_) => {
                    let resource = desktop_resource.context("Desktop resource is None")?;

                    // Check if there's actually a new frame
                    if frame_info.LastPresentTime == 0 {
                        self.duplication.ReleaseFrame().ok();
                        return Ok(None);
                    }

                    let texture: ID3D11Texture2D = resource.cast()
                        .context("Failed to cast to ID3D11Texture2D")?;

                    Ok(Some((texture, frame_info)))
                }
                Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
                    // No new frame available
                    Ok(None)
                }
                Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
                    // Desktop duplication was invalidated, need to recreate
                    tracing::warn!("Desktop duplication access lost, will need to recreate");
                    Err(anyhow::anyhow!("Access lost"))
                }
                Err(e) => {
                    Err(e).context("Failed to acquire frame")
                }
            }
        }
    }

    /// Copy frame data to CPU-accessible memory
    fn copy_frame_data(&mut self, texture: &ID3D11Texture2D) -> Result<Vec<u8>> {
        unsafe {
            // Get or create staging texture
            let staging = self.get_staging_texture(texture)?.clone();

            // Copy from GPU texture to staging texture
            self.context.CopyResource(&staging, texture);

            // Map the staging texture for CPU read
            let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
            self.context
                .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
                .context("Failed to map staging texture")?;

            // Copy pixel data
            let src_pitch = mapped.RowPitch as usize;
            let dst_pitch = (self.width * 4) as usize;
            let height = self.height as usize;

            let mut data = vec![0u8; dst_pitch * height];

            let src_ptr = mapped.pData as *const u8;
            for y in 0..height {
                let src_row = src_ptr.add(y * src_pitch);
                let dst_row = data.as_mut_ptr().add(y * dst_pitch);
                ptr::copy_nonoverlapping(src_row, dst_row, dst_pitch);
            }

            // Unmap
            self.context.Unmap(&staging, 0);

            Ok(data)
        }
    }

    /// Extract dirty rectangles from frame info
    fn extract_dirty_rects(&self, _frame_info: &DXGI_OUTDUPL_FRAME_INFO) -> Option<Vec<DirtyRect>> {
        // TODO: Implement dirty rectangle extraction using
        // IDXGIOutputDuplication::GetFrameDirtyRects and GetFrameMoveRects
        // For now, return None to indicate full frame update
        None
    }
}

impl Capturer for DxgiCapturer {
    fn capture(&mut self) -> Result<Option<CapturedFrame>> {
        // Try to acquire a frame with 100ms timeout
        let frame_result = self.acquire_frame(100)?;

        let (texture, frame_info) = match frame_result {
            Some((t, f)) => (t, f),
            None => return Ok(None), // No new frame
        };

        // Copy frame data to CPU memory
        let data = self.copy_frame_data(&texture)?;

        // Release the frame
        unsafe {
            self.duplication.ReleaseFrame().ok();
        }

        // Extract dirty rectangles if available
        let dirty_rects = self.extract_dirty_rects(&frame_info);

        Ok(Some(CapturedFrame {
            width: self.width,
            height: self.height,
            data,
            timestamp: Instant::now(),
            display_id: self.display.id,
            dirty_rects,
        }))
    }

    fn display(&self) -> &Display {
        &self.display
    }

    fn is_valid(&self) -> bool {
        // Could check if duplication is still valid
        true
    }
}

impl Drop for DxgiCapturer {
    fn drop(&mut self) {
        // Release any held frame
        unsafe {
            self.duplication.ReleaseFrame().ok();
        }
    }
}


I see the issue now. The DXGI Desktop Duplication can fail silently or panic in certain scenarios on Geovision systems. The main potential issues are:

1. **Desktop Duplication access requires proper thread context** - The capture needs to run on a thread with access to the desktop.

2. **DXGI duplication can fail on machines with unusual graphics setups** - Geovision systems often have multiple graphics outputs or specialized capture cards.

3. **The agent might panic/crash before logging** - the crash happens before the tracing message gets flushed.

Let me add better error handling and logging to help diagnose. But first, let me check if there's a way to see what happened on the target machine.

Actually, looking at the code, the issue might be:
1. `init_streaming()` might panic during DXGI initialization
2. The agent doesn't have a robust panic handler to report crashes

Let me add more defensive code and better error reporting. First, I should build a new version of the agent with more logging and possibly try GDI capture instead of DXGI since Geovision might have issues with Desktop Duplication.

Let me make a debug build that:
1. Forces GDI mode (bypasses DXGI)
2. Has more verbose logging around streaming initialization

Actually, the simplest approach would be to update the config to disable DXGI and see if GDI works. Let me check the config structure first.


The DXGI capture on Geovision systems might be failing due to the specialized graphics hardware. Let me create a debug build that forces GDI mode and has better error logging. First, let me check the config: