Skip to main content

netget/state/
app_state.rs

1//! Application state management
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::{mpsc, oneshot, RwLock};
6
7use super::client::{ClientId, ClientInstance};
8use super::easy::{EasyId, EasyInstance, EasyStatus};
9use super::server::{ServerId, ServerInstance};
10use super::task::{ScheduledTask, TaskId};
11use crate::server::connection::ConnectionId;
12
13/// Own a started child until its handle reaches the task registry. Dropping a
14/// Tokio JoinHandle detaches; dropping an unfinished registration must abort.
15struct PendingTaskRegistration(Option<tokio::task::JoinHandle<()>>);
16
17impl PendingTaskRegistration {
18    fn into_handle(mut self) -> tokio::task::JoinHandle<()> {
19        self.0.take().expect("registration owns its task")
20    }
21}
22
23impl Drop for PendingTaskRegistration {
24    fn drop(&mut self) {
25        if let Some(handle) = &self.0 {
26            handle.abort();
27        }
28    }
29}
30
31/// Source of an LLM conversation
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum ConversationSource {
34    /// User input from the command line
35    User,
36    /// Network event from a protocol implementation
37    Network {
38        server_id: ServerId,
39        connection_id: Option<ConnectionId>,
40    },
41    /// A model deciding what a network client should do next.
42    Client { client_id: ClientId },
43    /// Scheduled task execution
44    Task { task_name: String },
45    /// Scripting mode execution
46    Scripting,
47}
48
49impl ConversationSource {
50    pub fn display_label(&self) -> String {
51        match self {
52            ConversationSource::User => "[User]".to_string(),
53            ConversationSource::Network {
54                server_id,
55                connection_id,
56            } => {
57                if let Some(conn_id) = connection_id {
58                    format!("[Net #{}:{}]", server_id.as_u32(), conn_id)
59                } else {
60                    format!("[Net #{}]", server_id.as_u32())
61                }
62            }
63            ConversationSource::Task { task_name } => format!("[Task:{}]", task_name),
64            ConversationSource::Client { client_id } => format!("[Client #{}]", client_id.as_u32()),
65            ConversationSource::Scripting => "[Scripting]".to_string(),
66        }
67    }
68}
69
70/// Information about an active or recently-completed conversation
71#[derive(Debug, Clone)]
72pub struct ConversationInfo {
73    /// Unique conversation ID
74    pub id: String,
75    /// Source of the conversation
76    pub source: ConversationSource,
77    /// Details text (truncated input, context, etc.)
78    pub details: String,
79    /// When the conversation started
80    pub start_time: crate::utils::clock::Instant,
81    /// When the conversation ended (None if still active)
82    pub end_time: Option<crate::utils::clock::Instant>,
83}
84
85/// Operating mode for the application
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Mode {
88    /// Server mode - one or more servers listening
89    Server,
90    /// Client mode - connecting to remote servers
91    Client,
92    /// Idle - not yet configured
93    Idle,
94}
95
96impl Mode {
97    pub fn as_str(&self) -> &'static str {
98        match self {
99            Self::Server => "Server",
100            Self::Client => "Client",
101            Self::Idle => "Idle",
102        }
103    }
104}
105
106impl std::fmt::Display for Mode {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{}", self.as_str())
109    }
110}
111
112/// Selected scripting mode - which environment is active for the LLM
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum ScriptingMode {
115    /// LLM chooses runtime for each script
116    On,
117    /// Scripting disabled - LLM only mode
118    Off,
119    /// Python scripting enabled
120    Python,
121    /// JavaScript scripting enabled
122    JavaScript,
123    /// Go scripting enabled
124    Go,
125    /// Perl scripting enabled
126    Perl,
127}
128
129impl ScriptingMode {
130    pub fn as_str(&self) -> &'static str {
131        match self {
132            Self::On => "On",
133            Self::Off => "Off",
134            Self::Python => "Python",
135            Self::JavaScript => "JavaScript",
136            Self::Go => "Go",
137            Self::Perl => "Perl",
138        }
139    }
140}
141
142impl std::fmt::Display for ScriptingMode {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "{}", self.as_str())
145    }
146}
147
148/// Web search mode - controls when web search is allowed
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
150pub enum WebSearchMode {
151    /// Web search always enabled
152    #[default]
153    On,
154    /// Web search always disabled
155    Off,
156    /// Ask user for approval before each search
157    Ask,
158}
159
160impl WebSearchMode {
161    pub fn as_str(&self) -> &'static str {
162        match self {
163            Self::On => "on",
164            Self::Off => "off",
165            Self::Ask => "ask",
166        }
167    }
168}
169
170impl std::fmt::Display for WebSearchMode {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        write!(f, "{}", self.as_str())
173    }
174}
175
176impl std::str::FromStr for WebSearchMode {
177    type Err = String;
178
179    fn from_str(s: &str) -> Result<Self, Self::Err> {
180        match s.to_lowercase().as_str() {
181            "on" | "enabled" | "enable" | "true" => Ok(Self::On),
182            "off" | "disabled" | "disable" | "false" => Ok(Self::Off),
183            "ask" => Ok(Self::Ask),
184            _ => Err(format!("Invalid web search mode: {}", s)),
185        }
186    }
187}
188
189/// Event handler mode - controls how LLM should configure event handlers
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum EventHandlerMode {
192    /// LLM chooses handler types (script/static/llm) as appropriate
193    #[default]
194    Any,
195    /// Force all events to use script handlers
196    Script,
197    /// Force all events to use static response handlers
198    Static,
199    /// Force all events to use LLM handlers
200    Llm,
201}
202
203impl EventHandlerMode {
204    pub fn as_str(&self) -> &'static str {
205        match self {
206            Self::Any => "ANY",
207            Self::Script => "SCRIPT",
208            Self::Static => "STATIC",
209            Self::Llm => "LLM",
210        }
211    }
212}
213
214impl std::fmt::Display for EventHandlerMode {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        write!(f, "{}", self.as_str())
217    }
218}
219
220impl std::str::FromStr for EventHandlerMode {
221    type Err = String;
222
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        match s.to_lowercase().as_str() {
225            "any" => Ok(Self::Any),
226            "script" => Ok(Self::Script),
227            "static" => Ok(Self::Static),
228            "llm" => Ok(Self::Llm),
229            _ => Err(format!("Invalid event handler mode: {}", s)),
230        }
231    }
232}
233
234/// Request for web search approval (sent from tool executor to UI)
235pub struct WebApprovalRequest {
236    pub url: String,
237    pub response_tx: oneshot::Sender<WebApprovalResponse>,
238}
239
240/// Response from user for web search approval
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum WebApprovalResponse {
243    /// Allow this single request
244    Allow,
245    /// Deny this request
246    Deny,
247    /// Always allow (switch to ON mode for session)
248    AlwaysAllow,
249}
250
251/// A single request/response access-log entry.
252///
253/// Captured for every network event a protocol server handles: the request is
254/// the structured event data, the response is the action JSON the LLM (or a
255/// script/static handler) produced. Used by the `list_access_logs` /
256/// `get_access_log` MCP tools for debugging.
257#[derive(Clone, Debug, serde::Serialize)]
258pub struct AccessLogEntry {
259    /// Monotonic entry id (assigned in arrival order)
260    pub id: u64,
261    /// Unix time in milliseconds when the entry was recorded
262    pub unix_ms: u64,
263    /// Server that handled the request (absent on client-side entries).
264    /// Serialization skips `None`, so pre-existing server entries keep their
265    /// exact JSON shape for MCP consumers.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub server_id: Option<u32>,
268    /// Client that produced/handled the event (absent on server-side entries)
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub client_id: Option<u32>,
271    /// Protocol name (e.g. "http", "dns")
272    pub protocol: String,
273    /// Connection the request arrived on, if the protocol is connection-oriented
274    pub connection_id: Option<u32>,
275    /// Event type id (e.g. "http_request")
276    pub event_type: String,
277    /// The request: structured event data
278    pub request: serde_json::Value,
279    /// The response: action JSON produced for this request.
280    ///
281    /// Server entries record actions as executed (failures appear as
282    /// `FAILED: <action>` envelopes). Client entries record the actions the
283    /// handler/LLM *produced* — execution happens later inside the client's
284    /// connection loop, which does not report back here.
285    pub response: Vec<serde_json::Value>,
286}
287
288impl AccessLogEntry {
289    /// Which instance this entry belongs to.
290    pub fn owner(&self) -> Option<AccessLogOwner> {
291        match (self.server_id, self.client_id) {
292            (Some(id), _) => Some(AccessLogOwner::Server(id)),
293            (None, Some(id)) => Some(AccessLogOwner::Client(id)),
294            (None, None) => None,
295        }
296    }
297}
298
299/// The instance an access-log entry belongs to.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum AccessLogOwner {
302    Server(u32),
303    Client(u32),
304}
305
306/// Maximum number of access-log entries retained in memory (ring buffer).
307const ACCESS_LOG_CAPACITY: usize = 1000;
308
309/// One instance's accumulated feedback, taken out of its buffer for processing.
310///
311/// Exactly one of `server_id` / `client_id` is set.
312#[derive(Debug, Clone)]
313pub struct DueFeedback {
314    /// The server this feedback belongs to, if it is server feedback
315    pub server_id: Option<ServerId>,
316    /// The client this feedback belongs to, if it is client feedback
317    pub client_id: Option<super::ClientId>,
318    /// The `feedback_instructions` the instance was started with
319    pub instructions: String,
320    /// The instance's current instruction, so the model can adjust it
321    pub instruction: String,
322    /// The instance's current memory
323    pub memory: String,
324    /// The accumulated entries, removed from the buffer
325    pub entries: Vec<serde_json::Value>,
326}
327
328/// Global application state
329#[derive(Clone)]
330pub struct AppState {
331    inner: Arc<RwLock<AppStateInner>>,
332    pub llm_activity: super::llm_activity::LlmActivityTracker,
333}
334
335struct AppStateInner {
336    /// Current operating mode
337    mode: Mode,
338    /// All server instances
339    servers: HashMap<ServerId, ServerInstance>,
340    /// All client instances
341    clients: HashMap<ClientId, ClientInstance>,
342    /// Tor client instances (for directory queries)
343    #[cfg(unix)]
344    tor_clients: HashMap<ClientId, Arc<arti_client::TorClient<tor_rtcompat::PreferredRuntime>>>,
345    /// All easy protocol instances
346    easy_instances: HashMap<EasyId, EasyInstance>,
347    /// Mapping from underlying server ID to easy ID (for event routing)
348    server_to_easy: HashMap<ServerId, EasyId>,
349    /// Mapping from underlying client ID to easy ID (for event routing)
350    client_to_easy: HashMap<ClientId, EasyId>,
351    /// Unified ID counter for servers, connections, and clients
352    /// This ensures all IDs are unique across all three types
353    next_unified_id: u32,
354    #[allow(dead_code)]
355    /// Next server ID to assign (DEPRECATED - use next_unified_id)
356    next_server_id: u32,
357    #[allow(dead_code)]
358    /// Next client ID to assign (DEPRECATED - use next_unified_id)
359    next_client_id: u32,
360    /// Current Ollama model (None = not yet selected/validated)
361    ollama_model: Option<String>,
362    /// Configured LLM client (with mock config, lock settings, etc.)
363    llm_client: Option<crate::llm::OllamaClient>,
364    /// Rate limiter for LLM calls (concurrency + token throttling)
365    rate_limiter: crate::llm::RateLimiter,
366    /// Available scripting environments (Python, Node.js)
367    scripting_env: crate::scripting::ScriptingEnvironment,
368    /// Currently selected scripting mode (LLM, Python, or JavaScript)
369    selected_scripting_mode: ScriptingMode,
370    /// Event handler mode (controls how LLM configures event handlers)
371    event_handler_mode: EventHandlerMode,
372    /// Web search mode (On/Off/Ask)
373    web_search_mode: WebSearchMode,
374    /// Channel for sending web approval requests to UI
375    web_approval_tx: Option<mpsc::UnboundedSender<WebApprovalRequest>>,
376    /// Whether to include disabled protocols (for testing)
377    include_disabled_protocols: bool,
378    /// Minimum protocol maturity that may be started (from `--min-stability`).
379    /// `None` means no gate — the default, current behaviour.
380    min_stability: Option<crate::protocol::metadata::DevelopmentState>,
381    /// Whether Ollama API locking is enabled (for concurrent test execution)
382    /// Ollama API base URL (default: http://localhost:11434)
383    ollama_url: String,
384    /// Unique instance ID for this NetGet process (for multi-instance isolation)
385    instance_id: String,
386    /// Scheduled tasks registry
387    tasks: HashMap<TaskId, ScheduledTask>,
388    /// In-flight scheduled executions, cancelled with their task or scope.
389    scheduled_executions: HashMap<TaskId, tokio::task::JoinHandle<()>>,
390    /// Workers cancelled when one peer closes, even while an injected write blocks.
391    peer_tasks: HashMap<(ServerId, u32), Vec<tokio::task::JoinHandle<()>>>,
392    /// Next task ID to assign
393    next_task_id: u64,
394    /// Conversation state for User Input Agent
395    user_conversation_state: Option<Arc<std::sync::Mutex<crate::llm::ConversationState>>>,
396    /// Task name to ID mapping (for user-friendly task_id strings)
397    task_names: HashMap<String, TaskId>,
398    /// System capabilities detected at startup
399    system_capabilities: crate::privilege::SystemCapabilities,
400    /// Active and recently-completed LLM conversations
401    conversations: Vec<ConversationInfo>,
402    /// Total LLM input tokens (prompt tokens)
403    total_input_tokens: u64,
404    /// Total LLM output tokens (completion tokens)
405    total_output_tokens: u64,
406    /// Total number of LLM calls made
407    total_llm_calls: u64,
408    /// SQLite database manager
409    _database_manager: crate::state::DatabaseManager,
410    /// Server protocols that have been documented (enables open_server for these protocols)
411    documented_server_protocols: std::collections::HashSet<String>,
412    /// Client protocols that have been documented (enables open_client for these protocols)
413    documented_client_protocols: std::collections::HashSet<String>,
414    /// Recent request/response access-log entries (bounded ring buffer)
415    access_logs: std::collections::VecDeque<AccessLogEntry>,
416    /// Next access-log entry id to assign
417    next_access_log_id: u64,
418    /// Declared pipes: `from` server's `on` event forwarded into `to` server.
419    /// Keyed by pipe id. Torn down when either endpoint server closes.
420    pipes: HashMap<crate::pipe::PipeId, crate::pipe::PipeSpec>,
421    /// Maximum LLM calls a single client session may make (0 = unlimited)
422    client_llm_call_limit: u32,
423    /// LLM calls consumed so far, per client session.
424    ///
425    /// Kept here rather than on `ClientInstance` so the budget is owned by the
426    /// component that enforces it; the entry is dropped by `remove_client`.
427    client_llm_calls: HashMap<ClientId, u32>,
428    /// Background tasks owned by each live client (read loops, transport drivers,
429    /// in-flight requests, …).
430    ///
431    /// A `Vec` per client, not a single slot: several protocols spawn more than one
432    /// task per connection (DNS spawns the hickory transport driver *and* the LLM
433    /// conversation task); retaining only the last one would leak the rest.
434    ///
435    /// Lives here rather than on `ClientInstance` because the handles are only ever
436    /// touched through `register_client_task` / `remove_client`, and `ClientInstance`
437    /// is constructed by callers outside this module.
438    client_tasks: HashMap<ClientId, Vec<tokio::task::JoinHandle<()>>>,
439    /// Background tasks owned by each live server (accept loops, datagram readers,
440    /// interface readers, …).
441    ///
442    /// A `Vec` per server for the same reason as `client_tasks`: a protocol may
443    /// legitimately own more than one long-lived loop. WireGuard has a UDP listener
444    /// and a TUN reader; OpenVPN had to fuse both under a single `select!` task
445    /// purely because the previous single-slot registry silently dropped whichever
446    /// handle was registered first, leaving `stop_server` able to abort only the
447    /// last one.
448    ///
449    /// Lives here rather than on `ServerInstance` because the handles are only ever
450    /// touched through `register_server_task` / `remove_server`, and `ServerInstance`
451    /// is constructed by callers outside this module.
452    server_tasks: HashMap<ServerId, Vec<tokio::task::JoinHandle<()>>>,
453    /// Type-erased handles to the *running* instance of each server.
454    ///
455    /// Async protocol actions are dispatched on the stateless protocol struct from
456    /// the registry, which has no channel or socket, so anything needing live state
457    /// could only return `NoAction`. This is where the live half is reachable from.
458    /// See `crate::state::server_handles` for the full rationale and the wiring
459    /// still needed in `src/llm/actions/`.
460    server_handles: HashMap<ServerId, crate::state::server_handles::ServerHandle>,
461    /// Command channels into running clients' connection loops (the dashboard's
462    /// \[send\] button and any future programmatic client control). See
463    /// `crate::state::client_handles` for the design.
464    client_handles: HashMap<ClientId, crate::state::client_handles::ClientHandle>,
465    /// Events parked by a `manual` handler, waiting for a human to compose the
466    /// answer at the dashboard. See `crate::state::intercepts`.
467    pub(crate) pending_intercepts: Vec<crate::state::intercepts::PendingIntercept>,
468    /// Monotonic id source for [`Self::pending_intercepts`].
469    pub(crate) next_intercept_id: u64,
470    /// Command channels into individual server connections, for protocols that
471    /// adopted peer messaging (the dashboard's "message this peer"). Reuses the
472    /// client command types: a command is just {action, reply}.
473    peer_handles: HashMap<(ServerId, u32), crate::state::client_handles::ClientHandle>,
474}
475
476impl AppStateInner {
477    fn remove_scheduled_task(&mut self, id: TaskId) -> Option<ScheduledTask> {
478        if let Some(handle) = self.scheduled_executions.remove(&id) {
479            handle.abort();
480        }
481        let task = self.tasks.remove(&id)?;
482        if self.task_names.get(&task.name) == Some(&id) {
483            self.task_names.remove(&task.name);
484        }
485        Some(task)
486    }
487
488    fn remove_peer(&mut self, server_id: ServerId, connection_id: u32) {
489        self.peer_handles.remove(&(server_id, connection_id));
490        for handle in self
491            .peer_tasks
492            .remove(&(server_id, connection_id))
493            .unwrap_or_default()
494        {
495            handle.abort();
496        }
497    }
498
499    /// Tear down everything a server owns, other than its own map entry.
500    ///
501    /// Called from every path that drops a server — [`AppState::remove_server`] and
502    /// [`AppState::cleanup_old_servers`] — so that no caller can forget one half of
503    /// it. Forgetting used to be easy and expensive: the MCP `stop_server` /
504    /// `stop_all` tools called `remove_server` without the scheduled-task cleanup the
505    /// TUI does, and every orphaned recurring task then kept firing on its interval,
506    /// each tick producing an LLM prompt for a server that no longer existed.
507    /// Abort everything the server owns. Returns the aborted listener and connection tasks:
508    /// an abort only *requests* cancellation, and the socket a task holds is closed when the
509    /// runtime next polls it, so a caller that needs the port free awaits these.
510    fn teardown_server(&mut self, server_id: ServerId) -> Vec<tokio::task::JoinHandle<()>> {
511        // Dropping a JoinHandle only DETACHES the task in Tokio, so the abort is what
512        // actually releases the listening socket.
513        let aborted = self.server_tasks.remove(&server_id).unwrap_or_default();
514        for handle in &aborted {
515            handle.abort();
516        }
517        // A live-instance handle must never outlive the server it points at.
518        self.server_handles.remove(&server_id);
519        self.peer_handles.retain(|(sid, _), _| *sid != server_id);
520        self.peer_tasks.retain(|(sid, _), handles| {
521            if *sid != server_id {
522                return true;
523            }
524            for handle in handles {
525                handle.abort();
526            }
527            false
528        });
529        self.drop_tasks_for_server(server_id);
530
531        // Tear down any pipe touching this server (as source or sink), mirroring
532        // scheduled-task scoping: a pipe cannot outlive either of its endpoints.
533        let doomed: Vec<crate::pipe::PipeId> = self
534            .pipes
535            .iter()
536            .filter(|(_, p)| p.from == server_id.as_u32() || p.to == server_id.as_u32())
537            .map(|(&pid, _)| pid)
538            .collect();
539        for pid in doomed {
540            self.pipes.remove(&pid);
541        }
542        aborted
543    }
544
545    /// Remove the scheduled tasks scoped to a server, including the tasks scoped to
546    /// that server's individual connections.
547    fn drop_tasks_for_server(&mut self, server_id: ServerId) {
548        use crate::state::task::TaskScope;
549
550        let task_ids_to_remove: Vec<TaskId> = self
551            .tasks
552            .iter()
553            .filter(|(_, task)| match &task.scope {
554                TaskScope::Server(sid) => *sid == server_id,
555                // A connection cannot outlive its server, so its tasks go too.
556                TaskScope::Connection(sid, _) => *sid == server_id,
557                _ => false,
558            })
559            .map(|(&id, _)| id)
560            .collect();
561
562        for id in task_ids_to_remove {
563            self.remove_scheduled_task(id);
564        }
565    }
566}
567
568/// Default ceiling on LLM calls per client session.
569///
570/// Clients drive their own event loops, so a response handler that always
571/// re-issues a request loops forever: the observed DNS-client runaway made 211
572/// model calls before the process died. 100 is well above any legitimate
573/// LLM-driven client session (the busiest real one — an SSH session issuing a
574/// command per response — is a handful of calls) while capping the blast radius of
575/// a non-converging loop to a bounded, affordable number of round-trips.
576///
577/// Override with `NETGET_CLIENT_LLM_CALL_LIMIT`; `0` disables the cap.
578pub const DEFAULT_CLIENT_LLM_CALL_LIMIT: u32 = 100;
579
580/// Read the per-client LLM call limit from the environment, falling back to
581/// [`DEFAULT_CLIENT_LLM_CALL_LIMIT`] when unset or unparseable.
582fn client_llm_call_limit_from_env() -> u32 {
583    std::env::var("NETGET_CLIENT_LLM_CALL_LIMIT")
584        .ok()
585        .and_then(|v| v.trim().parse::<u32>().ok())
586        .unwrap_or(DEFAULT_CLIENT_LLM_CALL_LIMIT)
587}
588
589impl AppState {
590    /// Create a new application state
591    pub fn new() -> Self {
592        Self::new_with_options(false, "http://localhost:11434".to_string())
593    }
594
595    /// Create a new application state with options
596    pub fn new_with_options(include_disabled_protocols: bool, ollama_url: String) -> Self {
597        // Detect scripting environments at startup
598        let scripting_env = crate::scripting::ScriptingEnvironment::detect();
599
600        // Detect system capabilities at startup
601        let system_capabilities = crate::privilege::SystemCapabilities::detect();
602
603        // Default to ON mode - LLM chooses runtime dynamically
604        let selected_scripting_mode = ScriptingMode::On;
605        let event_handler_mode = EventHandlerMode::default();
606
607        // Generate unique instance ID: process_id + timestamp + random bytes
608        let instance_id = Self::generate_instance_id();
609
610        // Create default rate limiter (will be configured from CLI args later)
611        let rate_limiter = crate::llm::RateLimiter::new(crate::llm::RateLimiterConfig::default());
612
613        Self {
614            llm_activity: super::llm_activity::LlmActivityTracker::default(),
615            inner: Arc::new(RwLock::new(AppStateInner {
616                mode: Mode::Idle,
617                servers: HashMap::new(),
618                clients: HashMap::new(),
619                #[cfg(unix)]
620                tor_clients: HashMap::new(),
621                easy_instances: HashMap::new(),
622                server_to_easy: HashMap::new(),
623                client_to_easy: HashMap::new(),
624                next_unified_id: 1,
625                next_server_id: 1,
626                next_client_id: 1,
627                ollama_model: None,
628                llm_client: None,
629                rate_limiter,
630                scripting_env,
631                selected_scripting_mode,
632                event_handler_mode,
633                web_search_mode: WebSearchMode::On, // Default to enabled
634                web_approval_tx: None,              // Will be set by TUI
635                include_disabled_protocols,
636                min_stability: None,
637                ollama_url,
638                instance_id,
639                tasks: HashMap::new(),
640                scheduled_executions: HashMap::new(),
641                peer_tasks: HashMap::new(),
642                next_task_id: 1,
643                user_conversation_state: None,
644                task_names: HashMap::new(),
645                system_capabilities,
646                conversations: Vec::new(),
647                total_input_tokens: 0,
648                total_output_tokens: 0,
649                total_llm_calls: 0,
650                _database_manager: crate::state::DatabaseManager::new(),
651                documented_server_protocols: std::collections::HashSet::new(),
652                documented_client_protocols: std::collections::HashSet::new(),
653                access_logs: std::collections::VecDeque::new(),
654                next_access_log_id: 1,
655                pipes: HashMap::new(),
656                client_llm_call_limit: client_llm_call_limit_from_env(),
657                client_llm_calls: HashMap::new(),
658                client_tasks: HashMap::new(),
659                server_tasks: HashMap::new(),
660                server_handles: HashMap::new(),
661                client_handles: HashMap::new(),
662                pending_intercepts: Vec::new(),
663                next_intercept_id: 1,
664                peer_handles: HashMap::new(),
665            })),
666        }
667    }
668
669    /// Generate a unique instance ID for this NetGet process
670    /// Format: claude-{pid}-{timestamp}-{random4}
671    fn generate_instance_id() -> String {
672        use crate::utils::clock::{SystemTime, UNIX_EPOCH};
673
674        let pid = crate::utils::clock::process_id();
675        let timestamp = SystemTime::now()
676            .duration_since(UNIX_EPOCH)
677            .unwrap()
678            .as_secs();
679
680        // Generate 4 random hex characters for additional uniqueness
681        let random_bytes: [u8; 2] = rand::random();
682        let random_hex = hex::encode(random_bytes);
683
684        format!("claude-{}-{}-{}", pid, timestamp, random_hex)
685    }
686
687    /// Get the current mode
688    pub async fn get_mode(&self) -> Mode {
689        self.inner.read().await.mode
690    }
691
692    /// Set the mode
693    pub async fn set_mode(&self, mode: Mode) {
694        self.inner.write().await.mode = mode;
695    }
696
697    /// Add a new server instance and return its ID
698    pub async fn add_server(&self, mut server: ServerInstance) -> ServerId {
699        let mut inner = self.inner.write().await;
700        let id = ServerId::new(inner.next_unified_id);
701        inner.next_unified_id += 1;
702        server.id = id;
703        inner.servers.insert(id, server);
704
705        // Set mode to Server if this is the first server
706        if inner.mode == Mode::Idle {
707            inner.mode = Mode::Server;
708        }
709
710        id
711    }
712
713    /// Remove a server instance, aborting its background tasks and cancelling the
714    /// scheduled tasks scoped to it.
715    ///
716    /// The cleanup is done here rather than left to callers: MCP's `stop_server` and
717    /// `stop_all` did the removal without it, so every server- and connection-scoped
718    /// scheduled task survived its server and kept firing on its interval.
719    pub async fn remove_server(&self, id: ServerId) -> Option<ServerInstance> {
720        let (server, aborted) = {
721            let mut inner = self.inner.write().await;
722            let server = inner.servers.remove(&id);
723            let aborted = inner.teardown_server(id);
724            // Set mode to Idle if no more servers and no clients
725            if inner.servers.is_empty() && inner.clients.is_empty() {
726                inner.mode = Mode::Idle;
727            }
728            (server, aborted)
729        };
730        // Outside the lock: wait for the aborted tasks to be dropped, so the listening socket
731        // is closed when this returns. A stop followed by a start on the same port otherwise
732        // races the runtime. Bounded, because a task stuck in blocking code never yields.
733        for handle in aborted {
734            let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle).await;
735        }
736        server
737    }
738
739    /// Declare a pipe: forward `from`'s `on` events into `to` as `as_action`,
740    /// rendering `map` templates against the event data.
741    ///
742    /// Fails cleanly if either endpoint does not exist, or if the edge would
743    /// close a cycle among existing pipes (including the self-loop `from == to`).
744    pub async fn add_pipe(
745        &self,
746        from: ServerId,
747        on: String,
748        to: ServerId,
749        as_action: String,
750        map: std::collections::BTreeMap<String, String>,
751    ) -> anyhow::Result<crate::pipe::PipeId> {
752        let mut inner = self.inner.write().await;
753        if !inner.servers.contains_key(&from) {
754            anyhow::bail!("pipe source server #{} does not exist", from.as_u32());
755        }
756        if !inner.servers.contains_key(&to) {
757            anyhow::bail!("pipe target server #{} does not exist", to.as_u32());
758        }
759        let existing: Vec<crate::pipe::PipeSpec> = inner.pipes.values().cloned().collect();
760        if crate::pipe::would_create_cycle(&existing, from.as_u32(), to.as_u32()) {
761            anyhow::bail!(
762                "pipe #{} -> #{} refused: it would create a delivery cycle",
763                from.as_u32(),
764                to.as_u32()
765            );
766        }
767        let id = inner.next_unified_id;
768        inner.next_unified_id += 1;
769        inner.pipes.insert(
770            id,
771            crate::pipe::PipeSpec {
772                id,
773                from: from.as_u32(),
774                on,
775                to: to.as_u32(),
776                as_action,
777                map,
778            },
779        );
780        Ok(id)
781    }
782
783    /// Remove a pipe by id, returning it if present.
784    pub async fn remove_pipe(&self, id: crate::pipe::PipeId) -> Option<crate::pipe::PipeSpec> {
785        self.inner.write().await.pipes.remove(&id)
786    }
787
788    /// All declared pipes (cloned).
789    pub async fn list_pipes(&self) -> Vec<crate::pipe::PipeSpec> {
790        self.inner.read().await.pipes.values().cloned().collect()
791    }
792
793    /// Pipes whose source is `from` and that tap `event_id`.
794    pub async fn pipes_matching(
795        &self,
796        from: ServerId,
797        event_id: &str,
798    ) -> Vec<crate::pipe::PipeSpec> {
799        self.inner
800            .read()
801            .await
802            .pipes
803            .values()
804            .filter(|p| p.from == from.as_u32() && p.on == event_id)
805            .cloned()
806            .collect()
807    }
808
809    /// Register a background task owned by a server so it can be aborted on stop.
810    ///
811    /// Protocol `spawn` implementations call this with the `JoinHandle` of every
812    /// long-lived task they spawn — accept loop, datagram reader, interface reader —
813    /// right after spawning it. Handles ACCUMULATE: registering a second task no
814    /// longer discards the first, so a protocol with two loops gets both aborted.
815    ///
816    /// Finished handles are pruned on each registration so a protocol that registers
817    /// a task per connection cannot grow the vector without bound.
818    ///
819    /// If the server was already removed (raced with stop), the task is aborted
820    /// immediately to avoid leaking the listener.
821    /// Cancellation, including dropping the returned future before its first
822    /// poll, aborts the child until ownership has transferred into the registry.
823    pub fn register_server_task(
824        &self,
825        id: ServerId,
826        handle: tokio::task::JoinHandle<()>,
827    ) -> impl std::future::Future<Output = ()> + Send + '_ {
828        // This guard must exist before the async block: an async fn would leave
829        // an unpolled future holding only the raw, detach-on-drop JoinHandle.
830        let pending = PendingTaskRegistration(Some(handle));
831        async move {
832            let mut inner = self.inner.write().await;
833            if !inner.servers.contains_key(&id) {
834                return;
835            }
836            let tasks = inner.server_tasks.entry(id).or_default();
837            tasks.retain(|h| !h.is_finished());
838            tasks.push(pending.into_handle());
839        }
840    }
841
842    /// Spawn a task the server owns and register it, in one call.
843    ///
844    /// **Prefer this over `tokio::spawn` + `register_server_task` for per-connection tasks.**
845    /// A measurement on 15 September 2026 found 301 `tokio::spawn` calls across
846    /// `src/server/*/mod.rs` against 159 `register_server_task` calls, in 102 protocols — and
847    /// the unregistered ones are almost entirely the per-connection tasks, because the accept
848    /// loop is the handle a protocol remembers to register and the connection it just accepted
849    /// is the one it forgets. The consequence is the one the root `CLAUDE.md` records:
850    /// `stop_server` releases the listening socket and **in-flight connections keep running**,
851    /// still answering, still costing LLM budget, on a server the operator has stopped.
852    ///
853    /// Two properties make this safe to call per connection, both inherited from
854    /// [`Self::register_server_task`]: finished handles are pruned on every registration, so
855    /// the vector cannot grow without bound however many connections arrive; and a task
856    /// spawned against an already-removed server is aborted immediately rather than leaking
857    /// past its owner, which closes the accept-then-stop race.
858    ///
859    /// Returns an [`tokio::task::AbortHandle`] for the rare caller that needs to cancel the
860    /// task itself before the server stops; most callers discard it.
861    pub async fn spawn_server_task<F>(&self, id: ServerId, future: F) -> tokio::task::AbortHandle
862    where
863        F: std::future::Future<Output = ()> + Send + 'static,
864    {
865        let handle = tokio::spawn(future);
866        let abort = handle.abort_handle();
867        self.register_server_task(id, handle).await;
868        abort
869    }
870
871    /// The client-side twin of [`Self::spawn_server_task`]. Same reasoning: a client that
872    /// spawns a task per request should not have to remember two calls.
873    pub async fn spawn_client_task<F>(&self, id: ClientId, future: F) -> tokio::task::AbortHandle
874    where
875        F: std::future::Future<Output = ()> + Send + 'static,
876    {
877        let handle = tokio::spawn(future);
878        let abort = handle.abort_handle();
879        self.register_client_task(id, handle).await;
880        abort
881    }
882
883    /// Register a handle to the *running* instance of a server, so protocol actions
884    /// can reach its live state.
885    ///
886    /// A protocol's `spawn()` calls this with whatever type carries its live state —
887    /// typically a struct of `mpsc::Sender`s and shared views. The type is erased
888    /// here so `AppState` never learns any protocol's types; the protocol recovers
889    /// it with [`Self::server_handle`].
890    ///
891    /// Registering against an already-removed server is a no-op: the handle is
892    /// dropped rather than resurrecting an entry for a server that is gone.
893    ///
894    /// # Reaching this from an action
895    ///
896    /// Async actions are dispatched through `Server::execute_action()` on the
897    /// *stateless* protocol struct, which is why they can only return `NoAction`
898    /// today. Making this registry reachable needs one change in
899    /// `src/llm/actions/`, both parts mechanical:
900    ///
901    /// ```ignore
902    /// // src/llm/actions/protocol_trait.rs, in `trait Server`:
903    /// fn execute_action_with_state(
904    ///     &self,
905    ///     action: serde_json::Value,
906    ///     state: AppState,                       // cheap: AppState is an Arc
907    ///     server_id: Option<crate::state::ServerId>,
908    /// ) -> std::pin::Pin<
909    ///     Box<dyn std::future::Future<Output = anyhow::Result<ActionResult>> + Send + '_>,
910    /// > {
911    ///     // Default keeps every existing protocol working untouched.
912    ///     let _ = (state, server_id);
913    ///     let result = self.execute_action(action);
914    ///     Box::pin(async move { result })
915    /// }
916    ///
917    /// // src/llm/actions/executor.rs, replacing the `proto.execute_action(...)` call:
918    /// match proto
919    ///     .execute_action_with_state(action.clone(), state.clone(), server_id)
920    ///     .await
921    /// { /* unchanged */ }
922    /// ```
923    ///
924    /// A protocol that needs live state then overrides `execute_action_with_state`,
925    /// calls `state.server_handle::<MyHandle>(server_id?)`, and talks to its own
926    /// running server. Nothing about the stateless path changes.
927    pub async fn register_server_handle<T: std::any::Any + Send + Sync>(
928        &self,
929        id: ServerId,
930        handle: std::sync::Arc<T>,
931    ) {
932        let mut inner = self.inner.write().await;
933        if !inner.servers.contains_key(&id) {
934            return;
935        }
936        inner.server_handles.insert(id, handle);
937    }
938
939    /// Look up the live-instance handle of a server, downcast to the protocol's own
940    /// handle type.
941    ///
942    /// Returns `None` if the server is gone, never registered a handle, or
943    /// registered one of a different type — a protocol asking for someone else's
944    /// handle gets a miss, not a panic.
945    pub async fn server_handle<T: std::any::Any + Send + Sync>(
946        &self,
947        id: ServerId,
948    ) -> Option<std::sync::Arc<T>> {
949        self.inner
950            .read()
951            .await
952            .server_handles
953            .get(&id)
954            .cloned()
955            .and_then(|h| h.downcast::<T>().ok())
956    }
957
958    /// Whether a server has registered a live-instance handle
959    pub async fn has_server_handle(&self, id: ServerId) -> bool {
960        self.inner.read().await.server_handles.contains_key(&id)
961    }
962
963    // ===== Manual-handler intercepts (see crate::state::intercepts) =====
964
965    /// Park an event for a human to answer. Returns the intercept id and the
966    /// receiver the dispatcher must await (under its own timeout).
967    pub async fn park_intercept(
968        &self,
969        owner: crate::state::intercepts::InterceptOwner,
970        connection_id: Option<u32>,
971        event_type: &str,
972        description: &str,
973        event_data: Option<serde_json::Value>,
974        timeout_secs: u64,
975    ) -> (u64, tokio::sync::oneshot::Receiver<Vec<serde_json::Value>>) {
976        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
977        let mut inner = self.inner.write().await;
978        let id = inner.next_intercept_id;
979        inner.next_intercept_id += 1;
980        inner.pending_intercepts.retain(|entry| !entry.is_dead());
981        inner
982            .pending_intercepts
983            .push(crate::state::intercepts::PendingIntercept {
984                id,
985                owner,
986                connection_id,
987                event_type: event_type.to_string(),
988                description: description.to_string(),
989                event_data,
990                created_unix_ms: crate::state::intercepts::now_unix_ms(),
991                timeout_secs,
992                reply_tx: Some(reply_tx),
993            });
994        (id, reply_rx)
995    }
996
997    /// Pending intercepts, oldest first, with dead entries pruned.
998    pub async fn list_intercepts(&self) -> Vec<crate::state::intercepts::InterceptView> {
999        let mut inner = self.inner.write().await;
1000        inner.pending_intercepts.retain(|entry| !entry.is_dead());
1001        inner.pending_intercepts.iter().map(|e| e.view()).collect()
1002    }
1003
1004    /// Answer a parked event with the operator's actions.
1005    ///
1006    /// Errors if the intercept is gone — already answered, dismissed, or the
1007    /// waiting dispatcher gave up (timeout / connection closed). The answer is
1008    /// then NOT delivered anywhere; the peer has already received the
1009    /// fail-closed reply.
1010    pub async fn resolve_intercept(
1011        &self,
1012        id: u64,
1013        actions: Vec<serde_json::Value>,
1014    ) -> std::result::Result<(), String> {
1015        let entry = {
1016            let mut inner = self.inner.write().await;
1017            let Some(index) = inner.pending_intercepts.iter().position(|e| e.id == id) else {
1018                return Err(format!("request #{id} is no longer waiting"));
1019            };
1020            inner.pending_intercepts.remove(index)
1021        };
1022        let Some(tx) = entry.reply_tx else {
1023            return Err(format!("request #{id} was already answered"));
1024        };
1025        tx.send(actions).map_err(|_| {
1026            format!(
1027                "request #{id} timed out before this answer — the peer already got the \
1028                 fail-closed reply"
1029            )
1030        })
1031    }
1032
1033    /// Refuse a parked event: the dispatcher sees "no answer" immediately and
1034    /// takes the fail-closed path instead of waiting out its timeout.
1035    pub async fn dismiss_intercept(&self, id: u64) -> bool {
1036        let mut inner = self.inner.write().await;
1037        let before = inner.pending_intercepts.len();
1038        // Dropping the entry drops its sender, which wakes the dispatcher's
1039        // `await` with RecvError.
1040        inner.pending_intercepts.retain(|e| e.id != id);
1041        inner.pending_intercepts.len() != before
1042    }
1043
1044    /// Drop a parked entry without answering (the dispatcher cleans up after
1045    /// its own timeout so the list never shows a question nobody is waiting on).
1046    pub async fn remove_intercept(&self, id: u64) {
1047        let mut inner = self.inner.write().await;
1048        inner.pending_intercepts.retain(|e| e.id != id);
1049    }
1050
1051    // ===== Peer command handles (message one server connection) =====
1052
1053    /// Register a command channel into one connection of a server. Only
1054    /// protocols that adopted peer messaging call this; the dashboard offers
1055    /// "message this peer" exactly where a handle exists.
1056    pub async fn register_peer_handle(
1057        &self,
1058        server_id: ServerId,
1059        connection_id: u32,
1060        handle: crate::state::client_handles::ClientHandle,
1061    ) {
1062        let mut inner = self.inner.write().await;
1063        if !inner.servers.contains_key(&server_id) {
1064            return;
1065        }
1066        inner
1067            .peer_handles
1068            .insert((server_id, connection_id), handle);
1069    }
1070
1071    /// Whether a connection accepts injected actions.
1072    pub async fn has_peer_handle(&self, server_id: ServerId, connection_id: u32) -> bool {
1073        self.inner
1074            .read()
1075            .await
1076            .peer_handles
1077            .contains_key(&(server_id, connection_id))
1078    }
1079
1080    /// Drop a connection's command handle (the owning loop does this when the
1081    /// connection closes).
1082    pub async fn remove_peer_handle(&self, server_id: ServerId, connection_id: u32) {
1083        self.inner
1084            .write()
1085            .await
1086            .remove_peer(server_id, connection_id);
1087    }
1088
1089    /// Track a peer worker under its connection. A close racing registration
1090    /// aborts it immediately; registration never revives a removed handle.
1091    pub fn register_peer_task(
1092        &self,
1093        server_id: ServerId,
1094        connection_id: u32,
1095        handle: tokio::task::JoinHandle<()>,
1096    ) -> impl std::future::Future<Output = ()> + Send + '_ {
1097        // Construct ownership before polling: dropping an unpolled registration
1098        // must abort the already-running worker, just like client/server tasks.
1099        let pending = PendingTaskRegistration(Some(handle));
1100        async move {
1101            let mut inner = self.inner.write().await;
1102            if !inner.servers.contains_key(&server_id)
1103                || !inner.peer_handles.contains_key(&(server_id, connection_id))
1104            {
1105                return;
1106            }
1107            let tasks = inner
1108                .peer_tasks
1109                .entry((server_id, connection_id))
1110                .or_default();
1111            tasks.retain(|task| !task.is_finished());
1112            tasks.push(pending.into_handle());
1113        }
1114    }
1115
1116    /// Execute one action inside a server connection's own task and wait for
1117    /// the outcome. Same lock discipline as `send_to_client`: clone the handle
1118    /// out, drop the guard, then await.
1119    pub async fn send_to_peer(
1120        &self,
1121        server_id: ServerId,
1122        connection_id: u32,
1123        action: serde_json::Value,
1124        timeout: std::time::Duration,
1125    ) -> anyhow::Result<crate::state::client_handles::ClientSendOutcome> {
1126        use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
1127
1128        let handle = {
1129            let inner = self.inner.read().await;
1130            inner.peer_handles.get(&(server_id, connection_id)).cloned()
1131        };
1132        let Some(handle) = handle else {
1133            anyhow::bail!(
1134                "connection #{connection_id} on server #{} does not accept injected actions",
1135                server_id.as_u32()
1136            );
1137        };
1138        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
1139        let command = ClientCommand { action, reply_tx };
1140        if handle.command_tx.try_send(command).is_err() {
1141            // Full queue or a closed channel: either way the peer is not
1142            // taking commands right now. Prune a dead entry so the UI stops
1143            // offering it.
1144            let mut inner = self.inner.write().await;
1145            if let Some(entry) = inner.peer_handles.get(&(server_id, connection_id)) {
1146                if entry.command_tx.is_closed() {
1147                    inner.peer_handles.remove(&(server_id, connection_id));
1148                    return Ok(ClientSendOutcome::Disconnected);
1149                }
1150            }
1151            anyhow::bail!("connection #{connection_id} is busy — try again");
1152        }
1153        match tokio::time::timeout(timeout, reply_rx).await {
1154            Ok(Ok(outcome)) => outcome,
1155            Ok(Err(_)) => Ok(ClientSendOutcome::Disconnected),
1156            Err(_) => anyhow::bail!("no outcome within {}s", timeout.as_secs()),
1157        }
1158    }
1159
1160    // ===== Client command handles (see crate::state::client_handles) =====
1161
1162    /// Register the command channel for a running client. No-op if the client
1163    /// row is already gone (raced with `stop_client`) — the caller's receiver
1164    /// is then the only reference and sends fail cleanly.
1165    pub async fn register_client_handle(
1166        &self,
1167        id: ClientId,
1168        handle: crate::state::client_handles::ClientHandle,
1169    ) {
1170        let mut inner = self.inner.write().await;
1171        if !inner.clients.contains_key(&id) {
1172            return;
1173        }
1174        inner.client_handles.insert(id, handle);
1175    }
1176
1177    /// The command handle for a client, if its loop registered one.
1178    pub async fn client_handle(
1179        &self,
1180        id: ClientId,
1181    ) -> Option<crate::state::client_handles::ClientHandle> {
1182        self.inner.read().await.client_handles.get(&id).cloned()
1183    }
1184
1185    /// Whether a running client accepts injected commands. The UI uses this to
1186    /// grey out \[send\] for clients that have not adopted the channel.
1187    pub async fn has_client_handle(&self, id: ClientId) -> bool {
1188        self.inner.read().await.client_handles.contains_key(&id)
1189    }
1190
1191    /// Drop a client's command handle (called by the loop on exit; also done
1192    /// by `remove_client`).
1193    pub async fn remove_client_handle(&self, id: ClientId) {
1194        self.inner.write().await.client_handles.remove(&id);
1195    }
1196
1197    /// Execute one action inside a running client's connection loop and wait
1198    /// for the outcome.
1199    ///
1200    /// Lock discipline: the handle is cloned out under the read guard and the
1201    /// guard dropped before any await on the channel.
1202    pub async fn send_to_client(
1203        &self,
1204        id: ClientId,
1205        action: serde_json::Value,
1206        timeout: std::time::Duration,
1207    ) -> anyhow::Result<crate::state::client_handles::ClientSendOutcome> {
1208        use crate::state::client_handles::ClientCommand;
1209
1210        let handle = match self.client_handle(id).await {
1211            Some(handle) => handle,
1212            None => {
1213                anyhow::bail!(
1214                    "client #{} does not accept injected commands \
1215                     (not running, or its protocol has not adopted the command channel)",
1216                    id.as_u32()
1217                )
1218            }
1219        };
1220
1221        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
1222        let command = ClientCommand { action, reply_tx };
1223
1224        // A full channel within the timeout means the loop is alive but busy;
1225        // a closed channel means the loop exited.
1226        match tokio::time::timeout(timeout, handle.command_tx.send(command)).await {
1227            Ok(Ok(())) => {}
1228            Ok(Err(_)) => anyhow::bail!(
1229                "client #{}'s connection loop is not running (channel closed)",
1230                id.as_u32()
1231            ),
1232            Err(_) => anyhow::bail!(
1233                "client #{} is busy (command queue full after {:?})",
1234                id.as_u32(),
1235                timeout
1236            ),
1237        }
1238
1239        match tokio::time::timeout(timeout, reply_rx).await {
1240            Ok(Ok(outcome)) => outcome,
1241            Ok(Err(_)) => anyhow::bail!(
1242                "client #{}'s connection loop dropped the command without executing it \
1243                 (loop shutting down)",
1244                id.as_u32()
1245            ),
1246            Err(_) => anyhow::bail!(
1247                "client #{} did not report an outcome within {:?}",
1248                id.as_u32(),
1249                timeout
1250            ),
1251        }
1252    }
1253
1254    /// Number of background tasks currently tracked for a server
1255    pub async fn server_task_count(&self, id: ServerId) -> usize {
1256        self.inner
1257            .read()
1258            .await
1259            .server_tasks
1260            .get(&id)
1261            .map(|t| t.len())
1262            .unwrap_or(0)
1263    }
1264
1265    /// Get a server instance (cloned)
1266    pub async fn get_server(&self, id: ServerId) -> Option<ServerInstance> {
1267        self.inner.read().await.servers.get(&id).cloned()
1268    }
1269
1270    /// Get all server IDs
1271    pub async fn get_all_server_ids(&self) -> Vec<ServerId> {
1272        self.inner.read().await.servers.keys().copied().collect()
1273    }
1274
1275    /// Get all servers (cloned)
1276    pub async fn get_all_servers(&self) -> Vec<ServerInstance> {
1277        self.inner.read().await.servers.values().cloned().collect()
1278    }
1279
1280    /// Update server status
1281    pub async fn update_server_status(&self, id: ServerId, status: super::server::ServerStatus) {
1282        if let Some(server) = self.inner.write().await.servers.get_mut(&id) {
1283            server.status = status;
1284            server.status_changed_at = crate::utils::clock::Instant::now();
1285        }
1286    }
1287
1288    /// Update server local listening address
1289    pub async fn update_server_local_addr(&self, id: ServerId, local_addr: std::net::SocketAddr) {
1290        if let Some(server) = self.inner.write().await.servers.get_mut(&id) {
1291            server.local_addr = Some(local_addr);
1292        }
1293    }
1294
1295    /// Get instruction for a specific server
1296    pub async fn get_instruction(&self, server_id: ServerId) -> Option<String> {
1297        self.inner
1298            .read()
1299            .await
1300            .servers
1301            .get(&server_id)
1302            .map(|s| s.instruction.clone())
1303    }
1304
1305    /// Set instruction for a specific server
1306    pub async fn set_instruction(&self, server_id: ServerId, instruction: String) {
1307        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1308            server.instruction = instruction;
1309        }
1310    }
1311
1312    /// Get memory for a specific server
1313    pub async fn get_memory(&self, server_id: ServerId) -> Option<String> {
1314        self.inner
1315            .read()
1316            .await
1317            .servers
1318            .get(&server_id)
1319            .map(|s| s.memory.clone())
1320    }
1321
1322    /// Set memory for a specific server
1323    pub async fn set_memory(&self, server_id: ServerId, memory: String) {
1324        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1325            server.memory = memory;
1326        }
1327    }
1328
1329    /// Append to memory for a specific server
1330    pub async fn append_memory(&self, server_id: ServerId, text: String) {
1331        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1332            if !server.memory.is_empty() {
1333                server.memory.push('\n');
1334            }
1335            server.memory.push_str(&text);
1336        }
1337    }
1338
1339    /// Add feedback to a server's feedback buffer
1340    ///
1341    /// Feedback is accumulated and processed later via debounced LLM invocation
1342    /// Returns Ok if feedback was added, Err if server not found or feedback_instructions not set
1343    pub async fn add_server_feedback(
1344        &self,
1345        server_id: ServerId,
1346        feedback: serde_json::Value,
1347    ) -> anyhow::Result<()> {
1348        let mut inner = self.inner.write().await;
1349        if let Some(server) = inner.servers.get_mut(&server_id) {
1350            // Only accumulate feedback if feedback_instructions is set
1351            if server.feedback_instructions.is_some() {
1352                server.feedback_buffer.push(feedback);
1353                Ok(())
1354            } else {
1355                anyhow::bail!(
1356                    "Server {} has no feedback_instructions configured",
1357                    server_id
1358                )
1359            }
1360        } else {
1361            anyhow::bail!("Server {} not found", server_id)
1362        }
1363    }
1364
1365    /// Take the feedback that is due for processing, emptying those buffers.
1366    ///
1367    /// Leading-edge debounce: an instance is due as soon as it has any buffered feedback
1368    /// and `debounce` has elapsed since its last processing (so the first feedback after a
1369    /// quiet period is acted on at the next tick, and a burst is coalesced into one call).
1370    /// `last_feedback_processed` is stamped here rather than after the LLM answers, so a
1371    /// slow or failing call cannot re-trigger itself on every tick.
1372    ///
1373    /// Draining here — under the same write lock that stamps the timestamp — is what makes
1374    /// the caller safe to run from a 1s timer: two ticks can never take the same entries.
1375    pub async fn take_due_feedback(&self, debounce: std::time::Duration) -> Vec<DueFeedback> {
1376        let now = crate::utils::clock::Instant::now();
1377        let mut due = Vec::new();
1378        let mut inner = self.inner.write().await;
1379
1380        for (id, server) in inner.servers.iter_mut() {
1381            let Some(instructions) = server.feedback_instructions.clone() else {
1382                continue;
1383            };
1384            if server.feedback_buffer.is_empty() {
1385                continue;
1386            }
1387            if let Some(last) = server.last_feedback_processed {
1388                if now.duration_since(last) < debounce {
1389                    continue;
1390                }
1391            }
1392            server.last_feedback_processed = Some(now);
1393            due.push(DueFeedback {
1394                server_id: Some(*id),
1395                client_id: None,
1396                instructions,
1397                instruction: server.instruction.clone(),
1398                memory: server.memory.clone(),
1399                entries: std::mem::take(&mut server.feedback_buffer),
1400            });
1401        }
1402
1403        for (id, client) in inner.clients.iter_mut() {
1404            let Some(instructions) = client.feedback_instructions.clone() else {
1405                continue;
1406            };
1407            if client.feedback_buffer.is_empty() {
1408                continue;
1409            }
1410            if let Some(last) = client.last_feedback_processed {
1411                if now.duration_since(last) < debounce {
1412                    continue;
1413                }
1414            }
1415            client.last_feedback_processed = Some(now);
1416            due.push(DueFeedback {
1417                server_id: None,
1418                client_id: Some(*id),
1419                instructions,
1420                instruction: client.instruction.clone(),
1421                memory: client.memory.clone(),
1422                entries: std::mem::take(&mut client.feedback_buffer),
1423            });
1424        }
1425
1426        due
1427    }
1428
1429    /// Add feedback to a client's feedback buffer
1430    ///
1431    /// Feedback is accumulated and processed later via debounced LLM invocation
1432    /// Returns Ok if feedback was added, Err if client not found or feedback_instructions not set
1433    pub async fn add_client_feedback(
1434        &self,
1435        client_id: super::ClientId,
1436        feedback: serde_json::Value,
1437    ) -> anyhow::Result<()> {
1438        let mut inner = self.inner.write().await;
1439        if let Some(client) = inner.clients.get_mut(&client_id) {
1440            // Only accumulate feedback if feedback_instructions is set
1441            if client.feedback_instructions.is_some() {
1442                client.feedback_buffer.push(feedback);
1443                Ok(())
1444            } else {
1445                anyhow::bail!(
1446                    "Client {} has no feedback_instructions configured",
1447                    client_id
1448                )
1449            }
1450        } else {
1451            anyhow::bail!("Client {} not found", client_id)
1452        }
1453    }
1454
1455    /// Get first server ID (synchronous version for use in non-async contexts)
1456    ///
1457    /// This is a non-blocking helper that attempts to get the first server ID
1458    /// Returns None if unable to acquire lock or no servers exist
1459    pub fn get_first_server_id_sync(&self) -> Option<ServerId> {
1460        // Try non-blocking read
1461        self.inner.try_read().ok()?.servers.keys().next().copied()
1462    }
1463
1464    /// Get the Ollama model name
1465    pub async fn get_ollama_model(&self) -> Option<String> {
1466        self.inner.read().await.ollama_model.clone()
1467    }
1468
1469    /// Set the Ollama model name
1470    pub async fn set_ollama_model(&self, model: Option<String>) {
1471        self.inner.write().await.ollama_model = model;
1472    }
1473
1474    /// Get the scripting environment information
1475    pub async fn get_scripting_env(&self) -> crate::scripting::ScriptingEnvironment {
1476        self.inner.read().await.scripting_env.clone()
1477    }
1478
1479    /// Set scripting environment
1480    ///
1481    /// This is primarily used for testing to inject a mock environment.
1482    /// In production, the environment is auto-detected at startup.
1483    pub async fn set_scripting_env(&self, env: crate::scripting::ScriptingEnvironment) {
1484        self.inner.write().await.scripting_env = env;
1485    }
1486
1487    /// Get the currently selected scripting mode
1488    pub async fn get_selected_scripting_mode(&self) -> ScriptingMode {
1489        self.inner.read().await.selected_scripting_mode
1490    }
1491
1492    /// Set the selected scripting mode
1493    pub async fn set_selected_scripting_mode(&self, mode: ScriptingMode) {
1494        self.inner.write().await.selected_scripting_mode = mode;
1495    }
1496
1497    /// Cycle between ON and OFF scripting modes
1498    /// Returns the new mode and whether any switch occurred
1499    pub async fn cycle_scripting_mode(&self) -> (ScriptingMode, bool) {
1500        let mut inner = self.inner.write().await;
1501        let current = inner.selected_scripting_mode;
1502
1503        // Toggle between ON and OFF only
1504        // If currently on a specific language, treat as ON and toggle to OFF
1505        let next = match current {
1506            ScriptingMode::On
1507            | ScriptingMode::Python
1508            | ScriptingMode::JavaScript
1509            | ScriptingMode::Go
1510            | ScriptingMode::Perl => ScriptingMode::Off,
1511            ScriptingMode::Off => ScriptingMode::On,
1512        };
1513
1514        inner.selected_scripting_mode = next;
1515        (next, true)
1516    }
1517
1518    /// Check if server documentation has been read (enables open_server)
1519    pub async fn is_server_docs_read(&self) -> bool {
1520        !self
1521            .inner
1522            .read()
1523            .await
1524            .documented_server_protocols
1525            .is_empty()
1526    }
1527
1528    /// Check if client documentation has been read (enables open_client)
1529    pub async fn is_client_docs_read(&self) -> bool {
1530        !self
1531            .inner
1532            .read()
1533            .await
1534            .documented_client_protocols
1535            .is_empty()
1536    }
1537
1538    /// Mark server protocols as documented
1539    pub async fn mark_server_protocols_documented(&self, protocols: &[String]) {
1540        let mut inner = self.inner.write().await;
1541        for protocol in protocols {
1542            if crate::llm::conversation_state::ConversationState::valid_documented_protocol(
1543                protocol,
1544            ) && inner.documented_server_protocols.len()
1545                < crate::llm::conversation_state::ConversationState::MAX_DOCUMENTED_PROTOCOLS
1546            {
1547                inner.documented_server_protocols.insert(protocol.clone());
1548            }
1549        }
1550    }
1551
1552    /// Mark client protocols as documented
1553    pub async fn mark_client_protocols_documented(&self, protocols: &[String]) {
1554        let mut inner = self.inner.write().await;
1555        for protocol in protocols {
1556            if crate::llm::conversation_state::ConversationState::valid_documented_protocol(
1557                protocol,
1558            ) && inner.documented_client_protocols.len()
1559                < crate::llm::conversation_state::ConversationState::MAX_DOCUMENTED_PROTOCOLS
1560            {
1561                inner.documented_client_protocols.insert(protocol.clone());
1562            }
1563        }
1564    }
1565
1566    /// Get all documented server protocols
1567    pub async fn get_documented_server_protocols(&self) -> std::collections::HashSet<String> {
1568        self.inner.read().await.documented_server_protocols.clone()
1569    }
1570
1571    /// Get all documented client protocols
1572    pub async fn get_documented_client_protocols(&self) -> std::collections::HashSet<String> {
1573        self.inner.read().await.documented_client_protocols.clone()
1574    }
1575
1576    /// Get the currently selected event handler mode
1577    pub async fn get_event_handler_mode(&self) -> EventHandlerMode {
1578        self.inner.read().await.event_handler_mode
1579    }
1580
1581    /// Set the event handler mode
1582    pub async fn set_event_handler_mode(&self, mode: EventHandlerMode) {
1583        self.inner.write().await.event_handler_mode = mode;
1584    }
1585
1586    /// Cycle event handler mode through ANY -> SCRIPT -> STATIC -> LLM -> ANY
1587    /// Returns the new mode
1588    pub async fn cycle_event_handler_mode(&self) -> EventHandlerMode {
1589        let mut inner = self.inner.write().await;
1590        inner.event_handler_mode = match inner.event_handler_mode {
1591            EventHandlerMode::Any => EventHandlerMode::Script,
1592            EventHandlerMode::Script => EventHandlerMode::Static,
1593            EventHandlerMode::Static => EventHandlerMode::Llm,
1594            EventHandlerMode::Llm => EventHandlerMode::Any,
1595        };
1596        inner.event_handler_mode
1597    }
1598
1599    /// Get the current web search mode
1600    pub async fn get_web_search_mode(&self) -> WebSearchMode {
1601        self.inner.read().await.web_search_mode
1602    }
1603
1604    /// Set the web search mode
1605    pub async fn set_web_search_mode(&self, mode: WebSearchMode) {
1606        self.inner.write().await.web_search_mode = mode;
1607    }
1608
1609    /// Cycle web search mode through ON -> ASK -> OFF -> ON and return the new state
1610    pub async fn cycle_web_search_mode(&self) -> WebSearchMode {
1611        let mut inner = self.inner.write().await;
1612        inner.web_search_mode = match inner.web_search_mode {
1613            WebSearchMode::On => WebSearchMode::Ask,
1614            WebSearchMode::Ask => WebSearchMode::Off,
1615            WebSearchMode::Off => WebSearchMode::On,
1616        };
1617        inner.web_search_mode
1618    }
1619
1620    /// Set the web approval channel (called by TUI on startup)
1621    pub async fn set_web_approval_channel(&self, tx: mpsc::UnboundedSender<WebApprovalRequest>) {
1622        self.inner.write().await.web_approval_tx = Some(tx);
1623    }
1624
1625    /// Get a clone of the web approval channel
1626    pub async fn get_web_approval_channel(
1627        &self,
1628    ) -> Option<mpsc::UnboundedSender<WebApprovalRequest>> {
1629        self.inner.read().await.web_approval_tx.clone()
1630    }
1631
1632    /// Get whether disabled protocols should be included
1633    pub async fn get_include_disabled_protocols(&self) -> bool {
1634        self.inner.read().await.include_disabled_protocols
1635    }
1636
1637    /// Get the minimum stability required to start a protocol, if any
1638    /// (`None` = no gate).
1639    pub async fn get_min_stability(&self) -> Option<crate::protocol::metadata::DevelopmentState> {
1640        self.inner.read().await.min_stability
1641    }
1642
1643    /// Set the minimum stability required to start a protocol (from
1644    /// `--min-stability`). `None` disables the gate.
1645    pub async fn set_min_stability(
1646        &self,
1647        min: Option<crate::protocol::metadata::DevelopmentState>,
1648    ) {
1649        self.inner.write().await.min_stability = min;
1650    }
1651
1652    /// Get the Ollama API base URL
1653    pub async fn get_ollama_url(&self) -> String {
1654        self.inner.read().await.ollama_url.clone()
1655    }
1656
1657    /// Set the configured LLM client
1658    pub async fn set_llm_client(&self, client: crate::llm::OllamaClient) {
1659        self.inner.write().await.llm_client = Some(client);
1660    }
1661
1662    /// Get the configured LLM client
1663    pub async fn get_llm_client(&self) -> Option<crate::llm::OllamaClient> {
1664        self.inner.read().await.llm_client.clone()
1665    }
1666
1667    /// Get the rate limiter
1668    pub async fn get_rate_limiter(&self) -> crate::llm::RateLimiter {
1669        self.inner.read().await.rate_limiter.clone()
1670    }
1671
1672    /// Configure the rate limiter
1673    pub async fn configure_rate_limiter(
1674        &self,
1675        config: crate::llm::RateLimiterConfig,
1676    ) -> anyhow::Result<()> {
1677        let rate_limiter = self.inner.read().await.rate_limiter.clone();
1678        rate_limiter.update_config(config).await
1679    }
1680
1681    /// Get the unique instance ID for this NetGet process
1682    pub async fn get_instance_id(&self) -> String {
1683        self.inner.read().await.instance_id.clone()
1684    }
1685
1686    /// Get system capabilities detected at startup
1687    pub async fn get_system_capabilities(&self) -> crate::privilege::SystemCapabilities {
1688        self.inner.read().await.system_capabilities.clone()
1689    }
1690
1691    /// Get the next unified ID (for connections)
1692    /// This ensures all IDs (servers, connections, clients) are unique across all types
1693    pub async fn get_next_unified_id(&self) -> u32 {
1694        let mut inner = self.inner.write().await;
1695        let id = inner.next_unified_id;
1696        inner.next_unified_id += 1;
1697        id
1698    }
1699
1700    /// Update event handler configuration for a server
1701    pub async fn set_event_handler_config(
1702        &self,
1703        server_id: ServerId,
1704        config: Option<crate::scripting::EventHandlerConfig>,
1705    ) {
1706        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1707            server.event_handler_config = config;
1708        }
1709    }
1710
1711    /// Get event handler configuration for a server
1712    pub async fn get_event_handler_config(
1713        &self,
1714        server_id: ServerId,
1715    ) -> Option<crate::scripting::EventHandlerConfig> {
1716        self.inner
1717            .read()
1718            .await
1719            .servers
1720            .get(&server_id)
1721            .and_then(|s| s.event_handler_config.clone())
1722    }
1723
1724    /// Get a summary of current state for LLM context
1725    pub async fn get_summary(&self) -> String {
1726        let inner = self.inner.read().await;
1727        let total_connections: usize = inner.servers.values().map(|s| s.connections.len()).sum();
1728        format!(
1729            "Mode: {}, Servers: {}, Clients: {}, Total Connections: {}",
1730            inner.mode,
1731            inner.servers.len(),
1732            inner.clients.len(),
1733            total_connections
1734        )
1735    }
1736
1737    /// Get protocol name for a server
1738    pub async fn get_protocol_name(&self, server_id: ServerId) -> Option<String> {
1739        self.inner
1740            .read()
1741            .await
1742            .servers
1743            .get(&server_id)
1744            .map(|s| s.protocol_name.clone())
1745    }
1746
1747    /// Reap idle bookkeeping entries on **connectionless** servers (UDP, raw
1748    /// IP, link-level): their "connections" are per-remote-address records
1749    /// that nothing ever closes, so staleness is the only signal.
1750    ///
1751    /// Connection-oriented servers are skipped on purpose. This used to run
1752    /// over every server, and an idle TCP-style connection — a telnet peer
1753    /// whose line was parked ten seconds for a human's manual answer, a BGP
1754    /// session between keepalives — was evicted from the state map and shown
1755    /// as `(closed)` while its socket was alive; every later stat update and
1756    /// the real close then targeted an entry that was already gone. Which
1757    /// servers qualify comes from `ProtocolMetadataV2::connectionless`, so a
1758    /// protocol the registry does not know is left alone.
1759    pub async fn cleanup_old_connections(&self, max_age_secs: u64) {
1760        let mut inner = self.inner.write().await;
1761        for server in inner.servers.values_mut() {
1762            let connectionless = crate::protocol::server_registry::registry()
1763                .resolve(&server.protocol_name)
1764                .map(|p| p.metadata().connectionless)
1765                .unwrap_or(false);
1766            if connectionless {
1767                server.cleanup_old_connections(max_age_secs);
1768            }
1769        }
1770    }
1771
1772    /// Cleanup old closed connections (removes connections that have been closed for more than max_age_secs)
1773    pub async fn cleanup_closed_connections(&self, max_age_secs: u64) {
1774        use super::server::ConnectionStatus;
1775        let now = crate::utils::clock::Instant::now();
1776
1777        let mut inner = self.inner.write().await;
1778        for server in inner.servers.values_mut() {
1779            let to_remove: Vec<crate::server::connection::ConnectionId> = server
1780                .connections
1781                .iter()
1782                .filter(|(_, conn)| {
1783                    conn.status == ConnectionStatus::Closed
1784                        && now.duration_since(conn.status_changed_at).as_secs() >= max_age_secs
1785                })
1786                .map(|(id, _)| *id)
1787                .collect();
1788
1789            for id in to_remove {
1790                server.remove_connection(id);
1791            }
1792        }
1793    }
1794
1795    /// Cleanup old stopped/error servers (removes servers that have been stopped/error for more than max_age_secs)
1796    ///
1797    /// Reaping a server here runs the same teardown as [`Self::remove_server`]:
1798    /// background tasks aborted, scoped scheduled tasks cancelled. It previously
1799    /// dropped the map entry alone, which detached (rather than aborted) whatever
1800    /// the server was still running and left its scheduled tasks firing.
1801    ///
1802    /// Must be driven by *some* loop or it never runs. The TUI ticks it; MCP mode has
1803    /// its own reaper (`mcp_stdio::tools::spawn_state_reaper`), without which an
1804    /// LLM-initiated `close_server` left entries in `AppState` forever.
1805    pub async fn cleanup_old_servers(&self, max_age_secs: u64) {
1806        use super::server::ServerStatus;
1807        let now = crate::utils::clock::Instant::now();
1808
1809        let mut inner = self.inner.write().await;
1810        let to_remove: Vec<ServerId> = inner
1811            .servers
1812            .iter()
1813            .filter(|(_, server)| {
1814                let age = now.duration_since(server.status_changed_at).as_secs();
1815                // Remove if stopped and status changed more than max_age_secs ago
1816                // (Error servers are removed immediately on failure)
1817                matches!(server.status, ServerStatus::Stopped) && age >= max_age_secs
1818            })
1819            .map(|(id, _)| *id)
1820            .collect();
1821
1822        for id in to_remove {
1823            inner.servers.remove(&id);
1824            // Detached on purpose: this sweep holds the state lock and must not wait.
1825            drop(inner.teardown_server(id));
1826        }
1827
1828        // Set mode to Idle if no more servers and no clients
1829        if inner.servers.is_empty() && inner.clients.is_empty() {
1830            inner.mode = Mode::Idle;
1831        }
1832    }
1833
1834    /// Add a connection to a specific server
1835    pub async fn add_connection_to_server(
1836        &self,
1837        server_id: ServerId,
1838        connection: super::server::ConnectionState,
1839    ) {
1840        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1841            server.add_connection(connection);
1842        }
1843    }
1844
1845    /// Mark a connection as closed (instead of removing it immediately)
1846    pub async fn close_connection_on_server(
1847        &self,
1848        server_id: ServerId,
1849        connection_id: crate::server::connection::ConnectionId,
1850    ) {
1851        use super::server::ConnectionStatus;
1852        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1853            if let Some(conn) = server.get_connection_mut(connection_id) {
1854                conn.status = ConnectionStatus::Closed;
1855                conn.status_changed_at = crate::utils::clock::Instant::now();
1856            }
1857        }
1858
1859        self.remove_peer_handle(server_id, connection_id.as_u32())
1860            .await;
1861
1862        // Clean up any tasks associated with this connection
1863        self.cleanup_connection_tasks(server_id, connection_id)
1864            .await;
1865    }
1866
1867    /// Remove a connection from a specific server (used by cleanup task)
1868    pub async fn remove_connection_from_server(
1869        &self,
1870        server_id: ServerId,
1871        connection_id: crate::server::connection::ConnectionId,
1872    ) {
1873        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1874            server.remove_connection(connection_id);
1875        }
1876
1877        self.remove_peer_handle(server_id, connection_id.as_u32())
1878            .await;
1879
1880        // Clean up any tasks associated with this connection (safety measure)
1881        // Tasks should already be cleaned up when connection was closed,
1882        // but this ensures no orphaned tasks remain
1883        self.cleanup_connection_tasks(server_id, connection_id)
1884            .await;
1885    }
1886
1887    /// Recently closed connections for a server, newest first. Empty when the
1888    /// server does not exist.
1889    pub async fn get_recent_connections(
1890        &self,
1891        server_id: ServerId,
1892    ) -> Vec<super::server::ClosedConnectionSummary> {
1893        self.inner
1894            .read()
1895            .await
1896            .servers
1897            .get(&server_id)
1898            .map(|server| server.recent_connections.iter().cloned().collect())
1899            .unwrap_or_default()
1900    }
1901
1902    // ========== Proxy Filter Configuration Methods ==========
1903
1904    /// Get proxy filter configuration for a server
1905    #[cfg(unix)]
1906    pub async fn get_proxy_filter_config(
1907        &self,
1908        server_id: ServerId,
1909    ) -> Option<crate::server::proxy::filter::ProxyFilterConfig> {
1910        self.inner
1911            .read()
1912            .await
1913            .servers
1914            .get(&server_id)
1915            .and_then(|s| {
1916                s.protocol_data
1917                    .get("proxy_filter_config")
1918                    .and_then(|v| serde_json::from_value(v.clone()).ok())
1919            })
1920    }
1921
1922    /// Set proxy filter configuration for a server
1923    #[cfg(unix)]
1924    pub async fn set_proxy_filter_config(
1925        &self,
1926        server_id: ServerId,
1927        config: crate::server::proxy::filter::ProxyFilterConfig,
1928    ) {
1929        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1930            if let Ok(value) = serde_json::to_value(&config) {
1931                server.set_protocol_field("proxy_filter_config".to_string(), value);
1932            }
1933        }
1934    }
1935
1936    /// Update proxy filter configuration by merging with existing config
1937    #[cfg(unix)]
1938    pub async fn update_proxy_filter_config(
1939        &self,
1940        server_id: ServerId,
1941        update_fn: impl FnOnce(&mut crate::server::proxy::filter::ProxyFilterConfig),
1942    ) {
1943        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1944            let mut config = server
1945                .protocol_data
1946                .get("proxy_filter_config")
1947                .and_then(|v| serde_json::from_value(v.clone()).ok())
1948                .unwrap_or_else(crate::server::proxy::filter::ProxyFilterConfig::default);
1949
1950            update_fn(&mut config);
1951
1952            if let Ok(value) = serde_json::to_value(&config) {
1953                server.set_protocol_field("proxy_filter_config".to_string(), value);
1954            }
1955        }
1956    }
1957
1958    /// Get SOCKS5 filter configuration for a server
1959    #[cfg(not(target_arch = "wasm32"))]
1960    pub async fn get_socks5_filter_config(
1961        &self,
1962        server_id: ServerId,
1963    ) -> Option<crate::server::socks5::filter::Socks5FilterConfig> {
1964        self.inner
1965            .read()
1966            .await
1967            .servers
1968            .get(&server_id)
1969            .and_then(|s| {
1970                s.protocol_data
1971                    .get("socks5_filter_config")
1972                    .and_then(|v| serde_json::from_value(v.clone()).ok())
1973            })
1974    }
1975
1976    /// Set SOCKS5 filter configuration for a server
1977    #[cfg(not(target_arch = "wasm32"))]
1978    pub async fn set_socks5_filter_config(
1979        &self,
1980        server_id: ServerId,
1981        config: crate::server::socks5::filter::Socks5FilterConfig,
1982    ) {
1983        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
1984            if let Ok(value) = serde_json::to_value(&config) {
1985                server.set_protocol_field("socks5_filter_config".to_string(), value);
1986            }
1987        }
1988    }
1989
1990    /// Update SOCKS5 connection target address
1991    #[cfg(not(target_arch = "wasm32"))]
1992    pub async fn update_socks5_target(
1993        &self,
1994        server_id: ServerId,
1995        connection_id: ConnectionId,
1996        target_addr: Option<String>,
1997        username: Option<String>,
1998    ) {
1999        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2000            if let Some(conn) = server.connections.get_mut(&connection_id) {
2001                // Update fields in the flexible storage
2002                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2003                    if let Some(target) = target_addr {
2004                        obj.insert("target_addr".to_string(), serde_json::Value::String(target));
2005                    } else {
2006                        obj.insert("target_addr".to_string(), serde_json::Value::Null);
2007                    }
2008                    if let Some(user) = username {
2009                        obj.insert("username".to_string(), serde_json::Value::String(user));
2010                    } else {
2011                        obj.insert("username".to_string(), serde_json::Value::Null);
2012                    }
2013                }
2014            }
2015        }
2016    }
2017
2018    // ========== IMAP Connection State Methods ==========
2019
2020    /// Update IMAP connection session state
2021    #[cfg(not(target_arch = "wasm32"))]
2022    pub async fn update_imap_session_state(
2023        &self,
2024        server_id: ServerId,
2025        connection_id: ConnectionId,
2026        session_state: crate::state::server::ImapSessionState,
2027    ) {
2028        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2029            if let Some(conn) = server.connections.get_mut(&connection_id) {
2030                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2031                    obj.insert(
2032                        "session_state".to_string(),
2033                        serde_json::to_value(&session_state).unwrap_or(serde_json::Value::Null),
2034                    );
2035                }
2036            }
2037        }
2038    }
2039
2040    /// Update IMAP connection full state (session, user, mailbox)
2041    #[cfg(not(target_arch = "wasm32"))]
2042    pub async fn update_imap_connection_state(
2043        &self,
2044        server_id: ServerId,
2045        connection_id: ConnectionId,
2046        session_state: Option<crate::state::server::ImapSessionState>,
2047        authenticated_user: Option<Option<String>>,
2048        selected_mailbox: Option<Option<String>>,
2049        mailbox_read_only: Option<bool>,
2050    ) {
2051        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2052            if let Some(conn) = server.connections.get_mut(&connection_id) {
2053                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2054                    if let Some(s) = session_state {
2055                        obj.insert(
2056                            "session_state".to_string(),
2057                            serde_json::to_value(&s).unwrap_or(serde_json::Value::Null),
2058                        );
2059                    }
2060                    if let Some(u) = authenticated_user {
2061                        obj.insert(
2062                            "authenticated_user".to_string(),
2063                            serde_json::to_value(&u).unwrap_or(serde_json::Value::Null),
2064                        );
2065                    }
2066                    if let Some(m) = selected_mailbox {
2067                        obj.insert(
2068                            "selected_mailbox".to_string(),
2069                            serde_json::to_value(&m).unwrap_or(serde_json::Value::Null),
2070                        );
2071                    }
2072                    if let Some(r) = mailbox_read_only {
2073                        obj.insert("mailbox_read_only".to_string(), serde_json::Value::Bool(r));
2074                    }
2075                }
2076            }
2077        }
2078    }
2079
2080    /// Get IMAP connection state
2081    #[cfg(not(target_arch = "wasm32"))]
2082    pub async fn get_imap_connection_state(
2083        &self,
2084        server_id: ServerId,
2085        connection_id: ConnectionId,
2086    ) -> Option<(
2087        crate::state::server::ImapSessionState,
2088        Option<String>,
2089        Option<String>,
2090    )> {
2091        let inner = self.inner.read().await;
2092        if let Some(server) = inner.servers.get(&server_id) {
2093            if let Some(conn) = server.connections.get(&connection_id) {
2094                let session_state = conn
2095                    .protocol_info
2096                    .data
2097                    .get("session_state")
2098                    .and_then(|v| serde_json::from_value(v.clone()).ok())?;
2099                let authenticated_user = conn
2100                    .protocol_info
2101                    .data
2102                    .get("authenticated_user")
2103                    .and_then(|v| serde_json::from_value(v.clone()).ok());
2104                let selected_mailbox = conn
2105                    .protocol_info
2106                    .data
2107                    .get("selected_mailbox")
2108                    .and_then(|v| serde_json::from_value(v.clone()).ok());
2109                return Some((
2110                    session_state,
2111                    authenticated_user.flatten(),
2112                    selected_mailbox.flatten(),
2113                ));
2114            }
2115        }
2116        None
2117    }
2118
2119    /// Update IMAP protocol state (Idle/Processing/Accumulating)
2120    #[cfg(not(target_arch = "wasm32"))]
2121    pub async fn update_imap_protocol_state(
2122        &self,
2123        server_id: ServerId,
2124        connection_id: ConnectionId,
2125        protocol_state: crate::state::server::ProtocolState,
2126    ) {
2127        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2128            if let Some(conn) = server.connections.get_mut(&connection_id) {
2129                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2130                    obj.insert(
2131                        "state".to_string(),
2132                        serde_json::to_value(&protocol_state).unwrap_or(serde_json::Value::Null),
2133                    );
2134                }
2135            }
2136        }
2137    }
2138
2139    /// Update connection stats (bytes/packets)
2140    pub async fn update_connection_stats(
2141        &self,
2142        server_id: ServerId,
2143        connection_id: ConnectionId,
2144        bytes_received: Option<u64>,
2145        bytes_sent: Option<u64>,
2146        packets_received: Option<u64>,
2147        packets_sent: Option<u64>,
2148    ) {
2149        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2150            if let Some(conn) = server.connections.get_mut(&connection_id) {
2151                if let Some(br) = bytes_received {
2152                    conn.bytes_received += br;
2153                }
2154                if let Some(bs) = bytes_sent {
2155                    conn.bytes_sent += bs;
2156                }
2157                if let Some(pr) = packets_received {
2158                    conn.packets_received += pr;
2159                }
2160                if let Some(ps) = packets_sent {
2161                    conn.packets_sent += ps;
2162                }
2163                conn.last_activity = crate::utils::clock::Instant::now();
2164            }
2165        }
2166    }
2167
2168    /// Update connection status
2169    pub async fn update_connection_status(
2170        &self,
2171        server_id: ServerId,
2172        connection_id: ConnectionId,
2173        status: crate::state::server::ConnectionStatus,
2174    ) {
2175        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2176            if let Some(conn) = server.connections.get_mut(&connection_id) {
2177                conn.status = status;
2178                conn.status_changed_at = crate::utils::clock::Instant::now();
2179            }
2180        }
2181    }
2182
2183    /// Update VNC connection authentication status
2184    pub async fn update_vnc_connection_auth(
2185        &self,
2186        server_id: ServerId,
2187        connection_id: ConnectionId,
2188        authenticated: bool,
2189        username: Option<String>,
2190    ) {
2191        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2192            if let Some(conn) = server.connections.get_mut(&connection_id) {
2193                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2194                    obj.insert(
2195                        "authenticated".to_string(),
2196                        serde_json::Value::Bool(authenticated),
2197                    );
2198                    obj.insert(
2199                        "username".to_string(),
2200                        serde_json::to_value(&username).unwrap_or(serde_json::Value::Null),
2201                    );
2202                }
2203            }
2204        }
2205    }
2206
2207    /// Update Bitcoin connection info
2208    pub async fn update_bitcoin_connection_info(
2209        &self,
2210        server_id: ServerId,
2211        connection_id: ConnectionId,
2212        last_message_type: String,
2213    ) {
2214        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2215            if let Some(conn) = server.connections.get_mut(&connection_id) {
2216                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2217                    obj.insert(
2218                        "last_message_type".to_string(),
2219                        serde_json::Value::String(last_message_type.clone()),
2220                    );
2221                    // Mark handshake complete if we've seen both version and verack
2222                    if last_message_type == "verack" {
2223                        obj.insert(
2224                            "handshake_complete".to_string(),
2225                            serde_json::Value::Bool(true),
2226                        );
2227                    }
2228                }
2229            }
2230        }
2231    }
2232
2233    /// Update TFTP connection block number and total bytes
2234    pub async fn update_tftp_connection_block(
2235        &self,
2236        server_id: ServerId,
2237        connection_id: ConnectionId,
2238        block_number: u16,
2239        block_bytes: usize,
2240    ) {
2241        if let Some(server) = self.inner.write().await.servers.get_mut(&server_id) {
2242            if let Some(conn) = server.connections.get_mut(&connection_id) {
2243                if let Some(obj) = conn.protocol_info.data.as_object_mut() {
2244                    obj.insert(
2245                        "current_block".to_string(),
2246                        serde_json::Value::Number(block_number.into()),
2247                    );
2248                    // Update total bytes
2249                    let current_total =
2250                        obj.get("total_bytes").and_then(|v| v.as_u64()).unwrap_or(0);
2251                    obj.insert(
2252                        "total_bytes".to_string(),
2253                        serde_json::Value::Number((current_total + block_bytes as u64).into()),
2254                    );
2255                }
2256            }
2257        }
2258    }
2259
2260    /// Get VNC write half for sending framebuffer updates
2261    ///
2262    /// Note: This method is deprecated. Write halves are now managed locally
2263    /// within protocol modules and not stored in centralized state.
2264    pub async fn get_vnc_write_half(
2265        &self,
2266        _connection_id: ConnectionId,
2267    ) -> Option<std::sync::Arc<tokio::sync::Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>>
2268    {
2269        // Write halves are no longer stored in centralized state
2270        // Protocols manage their own local connection data for I/O
2271        None
2272    }
2273
2274    // ========== Client Management Methods ==========
2275
2276    /// Add a new client instance and return its ID
2277    pub async fn add_client(&self, mut client: ClientInstance) -> ClientId {
2278        let mut inner = self.inner.write().await;
2279        let id = ClientId::new(inner.next_unified_id);
2280        inner.next_unified_id += 1;
2281        client.id = id;
2282        inner.clients.insert(id, client);
2283
2284        // Set mode to Client if this is the first client (and no servers)
2285        if inner.mode == Mode::Idle {
2286            inner.mode = Mode::Client;
2287        }
2288
2289        id
2290    }
2291
2292    /// Remove a client instance, aborting every background task it owns.
2293    ///
2294    /// Dropping a `JoinHandle` only DETACHES the task in Tokio — it keeps running,
2295    /// keeps its socket open and keeps calling the LLM. Every handle registered via
2296    /// [`Self::register_client_task`] is therefore aborted explicitly here, which is
2297    /// what makes `stop_client` actually stop a client rather than just forgetting it.
2298    /// Disconnect a client but KEEP its row, so it can be reconnected later.
2299    ///
2300    /// Aborts the connection tasks (which closes the socket), drops the command
2301    /// handle, and marks the client Disconnected. Scheduled tasks and
2302    /// configuration survive — this is "hang up", not "remove".
2303    pub async fn disconnect_client(&self, id: ClientId) -> bool {
2304        {
2305            let mut inner = self.inner.write().await;
2306            if !inner.clients.contains_key(&id) {
2307                return false;
2308            }
2309            for handle in inner.client_tasks.remove(&id).unwrap_or_default() {
2310                handle.abort();
2311            }
2312            inner.client_handles.remove(&id);
2313        }
2314        self.update_client_status(id, super::client::ClientStatus::Disconnected)
2315            .await;
2316        true
2317    }
2318
2319    pub async fn remove_client(&self, id: ClientId) -> Option<ClientInstance> {
2320        let client = {
2321            let mut inner = self.inner.write().await;
2322            let client = inner.clients.remove(&id);
2323            inner.client_llm_calls.remove(&id);
2324            // A command handle must never outlive the client it points at.
2325            inner.client_handles.remove(&id);
2326
2327            for handle in inner.client_tasks.remove(&id).unwrap_or_default() {
2328                handle.abort();
2329            }
2330
2331            let task_ids: Vec<_> = inner
2332                .tasks
2333                .iter()
2334                .filter_map(|(task_id, task)| {
2335                    matches!(task.scope, crate::state::task::TaskScope::Client(cid) if cid == id)
2336                        .then_some(*task_id)
2337                })
2338                .collect();
2339            for task_id in task_ids {
2340                inner.remove_scheduled_task(task_id);
2341            }
2342
2343            // Set mode to Idle if no more clients and no servers
2344            if inner.clients.is_empty() && inner.servers.is_empty() {
2345                inner.mode = Mode::Idle;
2346            }
2347
2348            client
2349        };
2350        // Guard dropped above: resident-script teardown awaits child kills and
2351        // must not run under the state lock.
2352        crate::scripting::ResidentScriptManager::shutdown_client(id.as_u32()).await;
2353
2354        client
2355    }
2356
2357    /// Register a background task owned by a client so it can be aborted on stop.
2358    ///
2359    /// Client `connect()` implementations call this with the `JoinHandle` of every
2360    /// task they spawn — read loop, transport driver, in-flight request — right
2361    /// after spawning it. Like `register_server_task`, handles accumulate: a
2362    /// client that spawns three tasks gets all three aborted, none silently dropped.
2363    ///
2364    /// If the client is already gone (raced with `stop_client`), the task is aborted
2365    /// on the spot so it cannot outlive its owner.
2366    /// Dropping its future, even before polling or while waiting for the state
2367    /// lock, aborts the child until the registry has taken ownership.
2368    pub fn register_client_task(
2369        &self,
2370        id: ClientId,
2371        handle: tokio::task::JoinHandle<()>,
2372    ) -> impl std::future::Future<Output = ()> + Send + '_ {
2373        let pending = PendingTaskRegistration(Some(handle));
2374        async move {
2375            let mut inner = self.inner.write().await;
2376            if !inner.clients.contains_key(&id) {
2377                return;
2378            }
2379            let tasks = inner.client_tasks.entry(id).or_default();
2380            // Prune completed per-request tasks without growing the registry.
2381            tasks.retain(|h| !h.is_finished());
2382            tasks.push(pending.into_handle());
2383        }
2384    }
2385
2386    /// Number of background tasks currently tracked for a client
2387    pub async fn client_task_count(&self, id: ClientId) -> usize {
2388        self.inner
2389            .read()
2390            .await
2391            .client_tasks
2392            .get(&id)
2393            .map(|t| t.len())
2394            .unwrap_or(0)
2395    }
2396
2397    /// Consume one LLM call from a client's budget.
2398    ///
2399    /// Returns `Ok(calls_used)` while the client is under its cap, and `Err(message)`
2400    /// once the cap is reached. Clients hand-roll their own event loops and at least
2401    /// one of them (DNS) could re-enter the LLM path indefinitely; an unbounded loop
2402    /// burns real money against a paid endpoint, so the budget is a hard stop rather
2403    /// than a warning.
2404    ///
2405    /// The limit is per client session (not per event) and is configurable via
2406    /// [`Self::set_client_llm_call_limit`] or `NETGET_CLIENT_LLM_CALL_LIMIT`
2407    /// (`0` disables the cap entirely).
2408    pub async fn try_consume_client_llm_call(&self, id: ClientId) -> Result<u32, String> {
2409        let mut inner = self.inner.write().await;
2410        let limit = inner.client_llm_call_limit;
2411        if !inner.clients.contains_key(&id) {
2412            // Client already removed — nothing to budget. Let the caller proceed and
2413            // discover the removal through its own state checks.
2414            return Ok(0);
2415        }
2416        let used = inner.client_llm_calls.entry(id).or_insert(0);
2417
2418        if limit > 0 && *used >= limit {
2419            return Err(format!(
2420                "client {} exceeded its LLM call budget ({} calls). \
2421                 The client's event loop is not converging — it keeps invoking the model \
2422                 instead of terminating. Refusing further LLM calls; stop the client or \
2423                 raise NETGET_CLIENT_LLM_CALL_LIMIT (0 = unlimited).",
2424                id, limit
2425            ));
2426        }
2427
2428        *used += 1;
2429        Ok(*used)
2430    }
2431
2432    /// Number of LLM calls a client has consumed so far
2433    pub async fn get_client_llm_calls(&self, id: ClientId) -> u32 {
2434        self.inner
2435            .read()
2436            .await
2437            .client_llm_calls
2438            .get(&id)
2439            .copied()
2440            .unwrap_or(0)
2441    }
2442
2443    /// Get the per-client LLM call cap (0 = unlimited)
2444    pub async fn get_client_llm_call_limit(&self) -> u32 {
2445        self.inner.read().await.client_llm_call_limit
2446    }
2447
2448    /// Set the per-client LLM call cap (0 = unlimited)
2449    pub async fn set_client_llm_call_limit(&self, limit: u32) {
2450        self.inner.write().await.client_llm_call_limit = limit;
2451    }
2452
2453    /// Get a client instance (cloned)
2454    pub async fn get_client(&self, id: ClientId) -> Option<ClientInstance> {
2455        self.inner.read().await.clients.get(&id).cloned()
2456    }
2457
2458    /// Get all client IDs
2459    pub async fn get_all_client_ids(&self) -> Vec<ClientId> {
2460        self.inner.read().await.clients.keys().copied().collect()
2461    }
2462
2463    /// Get all clients (cloned)
2464    pub async fn get_all_clients(&self) -> Vec<ClientInstance> {
2465        self.inner.read().await.clients.values().cloned().collect()
2466    }
2467
2468    /// Update client status
2469    pub async fn update_client_status(&self, id: ClientId, status: super::client::ClientStatus) {
2470        if let Some(client) = self.inner.write().await.clients.get_mut(&id) {
2471            client.record_status_transition(&status);
2472            client.status = status;
2473            client.status_changed_at = crate::utils::clock::Instant::now();
2474        }
2475    }
2476
2477    /// Get instruction for a specific client
2478    pub async fn get_instruction_for_client(&self, client_id: ClientId) -> Option<String> {
2479        self.inner
2480            .read()
2481            .await
2482            .clients
2483            .get(&client_id)
2484            .map(|c| c.instruction.clone())
2485    }
2486
2487    /// Set instruction for a specific client
2488    pub async fn set_instruction_for_client(&self, client_id: ClientId, instruction: String) {
2489        if let Some(client) = self.inner.write().await.clients.get_mut(&client_id) {
2490            client.instruction = instruction;
2491        }
2492    }
2493
2494    /// Get memory for a specific client
2495    pub async fn get_memory_for_client(&self, client_id: ClientId) -> Option<String> {
2496        self.inner
2497            .read()
2498            .await
2499            .clients
2500            .get(&client_id)
2501            .map(|c| c.memory.clone())
2502    }
2503
2504    /// Set memory for a specific client
2505    pub async fn set_memory_for_client(&self, client_id: ClientId, memory: String) {
2506        if let Some(client) = self.inner.write().await.clients.get_mut(&client_id) {
2507            client.memory = memory;
2508        }
2509    }
2510
2511    /// Append to memory for a specific client
2512    pub async fn append_memory_for_client(&self, client_id: ClientId, text: String) {
2513        if let Some(client) = self.inner.write().await.clients.get_mut(&client_id) {
2514            if !client.memory.is_empty() {
2515                client.memory.push('\n');
2516            }
2517            client.memory.push_str(&text);
2518        }
2519    }
2520
2521    /// Get protocol name for a client
2522    pub async fn get_protocol_name_for_client(&self, client_id: ClientId) -> Option<String> {
2523        self.inner
2524            .read()
2525            .await
2526            .clients
2527            .get(&client_id)
2528            .map(|c| c.protocol_name.clone())
2529    }
2530
2531    /// Execute a closure with mutable access to a client
2532    pub async fn with_client_mut<F, R>(&self, client_id: ClientId, f: F) -> Option<R>
2533    where
2534        F: FnOnce(&mut ClientInstance) -> R,
2535    {
2536        self.inner.write().await.clients.get_mut(&client_id).map(f)
2537    }
2538
2539    /// Update event handler configuration for a client
2540    pub async fn set_client_event_handler_config(
2541        &self,
2542        client_id: ClientId,
2543        config: Option<crate::scripting::EventHandlerConfig>,
2544    ) {
2545        if let Some(client) = self.inner.write().await.clients.get_mut(&client_id) {
2546            client.event_handler_config = config;
2547        }
2548    }
2549
2550    /// Get event handler configuration for a client
2551    pub async fn get_client_event_handler_config(
2552        &self,
2553        client_id: ClientId,
2554    ) -> Option<crate::scripting::EventHandlerConfig> {
2555        self.inner
2556            .read()
2557            .await
2558            .clients
2559            .get(&client_id)
2560            .and_then(|c| c.event_handler_config.clone())
2561    }
2562
2563    /// Cleanup old disconnected clients (removes clients that have been disconnected for more than max_age_secs)
2564    pub async fn cleanup_old_clients(&self, max_age_secs: u64) {
2565        use super::client::ClientStatus;
2566        let now = crate::utils::clock::Instant::now();
2567
2568        let mut inner = self.inner.write().await;
2569        let to_remove: Vec<ClientId> = inner
2570            .clients
2571            .iter()
2572            .filter(|(_, client)| {
2573                let age = now.duration_since(client.status_changed_at).as_secs();
2574                // Remove if disconnected and status changed more than max_age_secs ago
2575                matches!(client.status, ClientStatus::Disconnected) && age >= max_age_secs
2576            })
2577            .map(|(id, _)| *id)
2578            .collect();
2579
2580        for id in to_remove {
2581            inner.clients.remove(&id);
2582        }
2583
2584        // Set mode to Idle if no more clients and no servers
2585        if inner.clients.is_empty() && inner.servers.is_empty() {
2586            inner.mode = Mode::Idle;
2587        }
2588    }
2589
2590    // ========== Easy Protocol Management ==========
2591
2592    /// Add a new easy protocol instance
2593    pub async fn add_easy_instance(&self, mut easy_instance: EasyInstance) -> EasyId {
2594        let mut inner = self.inner.write().await;
2595        let id = EasyId::new(inner.next_unified_id);
2596        inner.next_unified_id += 1;
2597        easy_instance.id = id;
2598        inner.easy_instances.insert(id, easy_instance);
2599        id
2600    }
2601
2602    /// Remove an easy protocol instance
2603    pub async fn remove_easy_instance(&self, id: EasyId) -> Option<EasyInstance> {
2604        let mut inner = self.inner.write().await;
2605        let instance = inner.easy_instances.remove(&id);
2606
2607        // Remove from routing maps if present
2608        if let Some(ref inst) = instance {
2609            if let Some(server_id) = inst.underlying_server_id {
2610                inner.server_to_easy.remove(&server_id);
2611            }
2612            if let Some(client_id) = inst.underlying_client_id {
2613                inner.client_to_easy.remove(&client_id);
2614            }
2615        }
2616
2617        instance
2618    }
2619
2620    /// Link an underlying server to an easy protocol instance (for event routing)
2621    pub async fn link_server_to_easy(&self, server_id: ServerId, easy_id: EasyId) {
2622        let mut inner = self.inner.write().await;
2623        inner.server_to_easy.insert(server_id, easy_id);
2624        if let Some(easy_instance) = inner.easy_instances.get_mut(&easy_id) {
2625            easy_instance.set_underlying_server(server_id);
2626        }
2627    }
2628
2629    /// Link an underlying client to an easy protocol instance (for event routing)
2630    pub async fn link_client_to_easy(&self, client_id: ClientId, easy_id: EasyId) {
2631        let mut inner = self.inner.write().await;
2632        inner.client_to_easy.insert(client_id, easy_id);
2633        if let Some(easy_instance) = inner.easy_instances.get_mut(&easy_id) {
2634            easy_instance.set_underlying_client(client_id);
2635        }
2636    }
2637
2638    /// Get easy ID for a given server ID (returns None if server is not managed by an easy protocol)
2639    pub async fn get_easy_for_server(&self, server_id: ServerId) -> Option<EasyId> {
2640        self.inner
2641            .read()
2642            .await
2643            .server_to_easy
2644            .get(&server_id)
2645            .copied()
2646    }
2647
2648    /// Get easy ID for a given client ID (returns None if client is not managed by an easy protocol)
2649    pub async fn get_easy_for_client(&self, client_id: ClientId) -> Option<EasyId> {
2650        self.inner
2651            .read()
2652            .await
2653            .client_to_easy
2654            .get(&client_id)
2655            .copied()
2656    }
2657
2658    /// Get easy protocol instance
2659    pub async fn get_easy_instance(&self, id: EasyId) -> Option<EasyInstance> {
2660        // Note: EasyInstance doesn't impl Clone because it contains JoinHandle
2661        // Instead we provide specific access methods
2662        self.inner.read().await.easy_instances.get(&id).map(|inst| {
2663            // Clone all fields except JoinHandle
2664            let mut cloned = EasyInstance::new(
2665                inst.id,
2666                inst.protocol_name.clone(),
2667                inst.underlying_protocol.clone(),
2668                inst.user_instruction.clone(),
2669            );
2670            cloned.underlying_server_id = inst.underlying_server_id;
2671            cloned.underlying_client_id = inst.underlying_client_id;
2672            cloned.status = inst.status.clone();
2673            cloned.created_at = inst.created_at;
2674            cloned.status_changed_at = inst.status_changed_at;
2675            // Note: handle is NOT cloned (cannot clone JoinHandle)
2676            cloned
2677        })
2678    }
2679
2680    /// Update easy protocol status
2681    pub async fn update_easy_status(&self, id: EasyId, status: EasyStatus) {
2682        if let Some(instance) = self.inner.write().await.easy_instances.get_mut(&id) {
2683            instance.set_status(status);
2684        }
2685    }
2686
2687    /// Get all easy protocol instances
2688    pub async fn get_all_easy_instances(&self) -> Vec<EasyInstance> {
2689        self.inner
2690            .read()
2691            .await
2692            .easy_instances
2693            .values()
2694            .map(|inst| {
2695                // Clone all fields except JoinHandle
2696                let mut cloned = EasyInstance::new(
2697                    inst.id,
2698                    inst.protocol_name.clone(),
2699                    inst.underlying_protocol.clone(),
2700                    inst.user_instruction.clone(),
2701                );
2702                cloned.underlying_server_id = inst.underlying_server_id;
2703                cloned.underlying_client_id = inst.underlying_client_id;
2704                cloned.status = inst.status.clone();
2705                cloned.created_at = inst.created_at;
2706                cloned.status_changed_at = inst.status_changed_at;
2707                cloned
2708            })
2709            .collect()
2710    }
2711
2712    /// Execute a closure with mutable access to an easy instance
2713    pub async fn with_easy_mut<F, R>(&self, easy_id: EasyId, f: F) -> Option<R>
2714    where
2715        F: FnOnce(&mut EasyInstance) -> R,
2716    {
2717        self.inner
2718            .write()
2719            .await
2720            .easy_instances
2721            .get_mut(&easy_id)
2722            .map(f)
2723    }
2724
2725    // ========== Backwards Compatibility Methods ==========
2726    // These methods help bridge old single-server code to new multi-server architecture
2727    // They typically operate on "the first server" or aggregate across all servers
2728
2729    /// Get the first server's port (for backwards compat)
2730    pub async fn get_port(&self) -> Option<u16> {
2731        self.inner
2732            .read()
2733            .await
2734            .servers
2735            .values()
2736            .next()
2737            .map(|s| s.port)
2738    }
2739
2740    /// Get the first server's protocol name (for backwards compat)
2741    pub async fn get_first_protocol_name(&self) -> Option<String> {
2742        self.inner
2743            .read()
2744            .await
2745            .servers
2746            .values()
2747            .next()
2748            .map(|s| s.protocol_name.clone())
2749    }
2750
2751    /// Get the first server's instruction (for backwards compat)
2752    pub async fn get_first_instruction(&self) -> String {
2753        self.inner
2754            .read()
2755            .await
2756            .servers
2757            .values()
2758            .next()
2759            .map(|s| s.instruction.clone())
2760            .unwrap_or_default()
2761    }
2762
2763    /// Get the first server's memory (for backwards compat)
2764    pub async fn get_first_memory(&self) -> String {
2765        self.inner
2766            .read()
2767            .await
2768            .servers
2769            .values()
2770            .next()
2771            .map(|s| s.memory.clone())
2772            .unwrap_or_default()
2773    }
2774
2775    /// Get the first server's ID (for backwards compat)
2776    pub async fn get_first_server_id(&self) -> Option<ServerId> {
2777        self.inner.read().await.servers.keys().next().copied()
2778    }
2779
2780    /// Get the first client's ID (for backwards compat)
2781    pub async fn get_first_client_id(&self) -> Option<ClientId> {
2782        self.inner.read().await.clients.keys().next().copied()
2783    }
2784
2785    /// Get local address of first server (for backwards compat)
2786    pub async fn get_local_addr(&self) -> Option<std::net::SocketAddr> {
2787        self.inner
2788            .read()
2789            .await
2790            .servers
2791            .values()
2792            .next()
2793            .and_then(|s| s.local_addr)
2794    }
2795
2796    /// Execute a closure with mutable access to a server
2797    pub async fn with_server_mut<F, R>(&self, server_id: ServerId, f: F) -> Option<R>
2798    where
2799        F: FnOnce(&mut ServerInstance) -> R,
2800    {
2801        self.inner.write().await.servers.get_mut(&server_id).map(f)
2802    }
2803
2804    // ===== Task Management Methods =====
2805
2806    /// Add a new scheduled task
2807    pub async fn add_task(&self, mut task: ScheduledTask) -> TaskId {
2808        let mut inner = self.inner.write().await;
2809        let id = TaskId::new(inner.next_task_id);
2810        inner.next_task_id += 1;
2811        // Constructors accept a placeholder ID. Every later status update and
2812        // completion uses the ID on the stored task, so it must match the map key.
2813        task.id = id;
2814
2815        inner.task_names.insert(task.name.clone(), id);
2816        inner.tasks.insert(id, task);
2817        id
2818    }
2819
2820    /// Get a task by ID or name
2821    pub async fn get_task(&self, id_or_name: &str) -> Option<ScheduledTask> {
2822        let inner = self.inner.read().await;
2823
2824        // Try parsing as numeric ID first
2825        if let Some(id) = TaskId::from_string(id_or_name) {
2826            return inner.tasks.get(&id).cloned();
2827        }
2828
2829        // Try looking up by name
2830        if let Some(&id) = inner.task_names.get(id_or_name) {
2831            return inner.tasks.get(&id).cloned();
2832        }
2833
2834        None
2835    }
2836
2837    /// Remove a task
2838    pub async fn remove_task(&self, id: TaskId) -> Option<ScheduledTask> {
2839        let mut inner = self.inner.write().await;
2840        inner.remove_scheduled_task(id)
2841    }
2842
2843    /// Start a claimed task while holding its ownership lock. Removal cannot
2844    /// race between spawn and registration; a previously removed claim is skipped.
2845    pub async fn spawn_scheduled_task<F>(&self, id: TaskId, future: F) -> bool
2846    where
2847        F: std::future::Future<Output = ()> + Send + 'static,
2848    {
2849        let mut inner = self.inner.write().await;
2850        if !inner
2851            .tasks
2852            .get(&id)
2853            .is_some_and(|task| task.status == crate::state::task::TaskStatus::Executing)
2854        {
2855            return false;
2856        }
2857        if inner
2858            .scheduled_executions
2859            .get(&id)
2860            .is_some_and(|handle| !handle.is_finished())
2861        {
2862            return false;
2863        }
2864        inner.scheduled_executions.insert(id, tokio::spawn(future));
2865        true
2866    }
2867
2868    /// Get all tasks
2869    pub async fn get_all_tasks(&self) -> Vec<ScheduledTask> {
2870        self.inner.read().await.tasks.values().cloned().collect()
2871    }
2872
2873    /// Atomically claim scheduled tasks whose deadline has arrived.
2874    ///
2875    /// A separate snapshot and status update lets concurrent timer ticks both
2876    /// execute the same task. Mark each task while holding the selection lock.
2877    pub async fn claim_due_tasks(&self) -> Vec<ScheduledTask> {
2878        use crate::state::task::TaskStatus;
2879        let mut inner = self.inner.write().await;
2880        let now = crate::utils::clock::Instant::now();
2881        let active: std::collections::HashSet<_> = inner
2882            .scheduled_executions
2883            .iter()
2884            .filter_map(|(id, handle)| (!handle.is_finished()).then_some(*id))
2885            .collect();
2886        inner
2887            .scheduled_executions
2888            .retain(|_, handle| !handle.is_finished());
2889        inner
2890            .tasks
2891            .values_mut()
2892            .filter_map(|task| {
2893                if task.status == TaskStatus::Scheduled
2894                    && task.next_execution <= now
2895                    && !active.contains(&task.id)
2896                {
2897                    task.status = TaskStatus::Executing;
2898                    Some(task.clone())
2899                } else {
2900                    None
2901                }
2902            })
2903            .collect()
2904    }
2905
2906    /// Get tasks for a specific server
2907    pub async fn get_server_tasks(&self, server_id: ServerId) -> Vec<ScheduledTask> {
2908        use crate::state::task::TaskScope;
2909
2910        self.inner
2911            .read()
2912            .await
2913            .tasks
2914            .values()
2915            .filter(|task| matches!(&task.scope, TaskScope::Server(sid) if *sid == server_id))
2916            .cloned()
2917            .collect()
2918    }
2919
2920    /// Get tasks for a specific connection
2921    pub async fn get_connection_tasks(
2922        &self,
2923        server_id: ServerId,
2924        connection_id: crate::server::connection::ConnectionId,
2925    ) -> Vec<ScheduledTask> {
2926        use crate::state::task::TaskScope;
2927
2928        self.inner
2929            .read()
2930            .await
2931            .tasks
2932            .values()
2933            .filter(|task| {
2934                matches!(
2935                    &task.scope,
2936                    TaskScope::Connection(sid, cid) if *sid == server_id && *cid == connection_id
2937                )
2938            })
2939            .cloned()
2940            .collect()
2941    }
2942
2943    /// Update task status
2944    pub async fn update_task_status(&self, id: TaskId, status: crate::state::task::TaskStatus) {
2945        if let Some(task) = self.inner.write().await.tasks.get_mut(&id) {
2946            task.status = status;
2947        }
2948    }
2949
2950    /// Update task next execution time
2951    pub async fn update_task_next_execution(&self, id: TaskId, next: crate::utils::clock::Instant) {
2952        if let Some(task) = self.inner.write().await.tasks.get_mut(&id) {
2953            task.next_execution = next;
2954        }
2955    }
2956
2957    /// Record task execution result
2958    pub async fn record_task_execution(
2959        &self,
2960        id: TaskId,
2961        result: &crate::state::task::TaskExecutionResult,
2962    ) {
2963        use crate::state::task::TaskType;
2964
2965        let mut inner = self.inner.write().await;
2966        if let Some(task) = inner.tasks.get_mut(&id) {
2967            if result.success {
2968                task.failure_count = 0;
2969                task.last_error = None;
2970            } else {
2971                task.failure_count = task.failure_count.saturating_add(1);
2972                task.last_error = result.error.clone();
2973            }
2974
2975            // Update execution count for recurring tasks
2976            if let TaskType::Recurring {
2977                ref mut executions_count,
2978                ..
2979            } = task.task_type
2980            {
2981                *executions_count = executions_count.saturating_add(1);
2982            }
2983        }
2984    }
2985
2986    /// Clean up the scheduled tasks associated with a server.
2987    ///
2988    /// Both server-scoped tasks and the connection-scoped tasks of *that server's*
2989    /// connections are removed: a connection cannot outlive its server, so leaving
2990    /// its tasks behind leaves them firing against a server that no longer exists.
2991    ///
2992    /// Callers rarely need this — [`Self::remove_server`] and
2993    /// [`Self::cleanup_old_servers`] already do it. It stays public for the TUI,
2994    /// which marks a server `Stopped` and leaves the entry in place for the reaper.
2995    pub async fn cleanup_server_tasks(&self, server_id: ServerId) {
2996        let mut inner = self.inner.write().await;
2997        inner.drop_tasks_for_server(server_id);
2998    }
2999
3000    /// Clean up tasks associated with a connection
3001    pub async fn cleanup_connection_tasks(
3002        &self,
3003        server_id: ServerId,
3004        connection_id: crate::server::connection::ConnectionId,
3005    ) {
3006        use crate::state::task::TaskScope;
3007
3008        let mut inner = self.inner.write().await;
3009
3010        // Collect task IDs to remove
3011        let task_ids_to_remove: Vec<TaskId> = inner
3012            .tasks
3013            .iter()
3014            .filter(|(_, task)| {
3015                matches!(
3016                    &task.scope,
3017                    TaskScope::Connection(sid, cid) if *sid == server_id && *cid == connection_id
3018                )
3019            })
3020            .map(|(&id, _)| id)
3021            .collect();
3022
3023        // Remove tasks and their name mappings
3024        for id in task_ids_to_remove {
3025            inner.remove_scheduled_task(id);
3026        }
3027    }
3028
3029    /// Clean up tasks associated with a client
3030    pub async fn cleanup_client_tasks(&self, client_id: ClientId) {
3031        use crate::state::task::TaskScope;
3032
3033        let mut inner = self.inner.write().await;
3034
3035        // Collect task IDs to remove
3036        let task_ids_to_remove: Vec<TaskId> = inner
3037            .tasks
3038            .iter()
3039            .filter(|(_, task)| matches!(&task.scope, TaskScope::Client(cid) if *cid == client_id))
3040            .map(|(&id, _)| id)
3041            .collect();
3042
3043        // Remove tasks and their name mappings
3044        for id in task_ids_to_remove {
3045            inner.remove_scheduled_task(id);
3046        }
3047    }
3048
3049    // ===== Conversation Management Methods =====
3050
3051    /// Register a new conversation
3052    pub async fn register_conversation(
3053        &self,
3054        id: String,
3055        source: ConversationSource,
3056        details: String,
3057    ) {
3058        let info = ConversationInfo {
3059            id,
3060            source,
3061            details,
3062            start_time: crate::utils::clock::Instant::now(),
3063            end_time: None,
3064        };
3065
3066        self.inner.write().await.conversations.push(info);
3067    }
3068
3069    /// Mark a conversation as ended
3070    pub async fn end_conversation(&self, id: &str) {
3071        let mut inner = self.inner.write().await;
3072        if let Some(conv) = inner.conversations.iter_mut().find(|c| c.id == id) {
3073            conv.end_time = Some(crate::utils::clock::Instant::now());
3074        }
3075    }
3076
3077    /// Get all active conversations and recently-completed ones (within 1 second)
3078    pub async fn get_active_conversations(&self) -> Vec<ConversationInfo> {
3079        let inner = self.inner.read().await;
3080        let now = crate::utils::clock::Instant::now();
3081
3082        inner
3083            .conversations
3084            .iter()
3085            .filter(|conv| {
3086                // Include if still active (no end_time)
3087                conv.end_time.is_none() ||
3088                // Or if ended within the last 1 second
3089                conv.end_time.map(|end| now.duration_since(end).as_secs() < 1).unwrap_or(false)
3090            })
3091            .cloned()
3092            .collect()
3093    }
3094
3095    /// Get conversations for the "Running" column (User + Global tasks)
3096    pub async fn get_input_conversations(&self) -> Vec<ConversationInfo> {
3097        let all_convs = self.get_active_conversations().await;
3098        all_convs
3099            .into_iter()
3100            .filter(|conv| {
3101                matches!(
3102                    &conv.source,
3103                    ConversationSource::User | ConversationSource::Scripting // Global tasks would be ConversationSource::Task but we need to filter by scope
3104                )
3105            })
3106            .collect()
3107    }
3108
3109    /// Clean up old completed conversations (older than 1 second)
3110    pub async fn cleanup_old_conversations(&self) {
3111        let mut inner = self.inner.write().await;
3112        let now = crate::utils::clock::Instant::now();
3113
3114        inner.conversations.retain(|conv| {
3115            // Keep if still active
3116            conv.end_time.is_none() ||
3117            // Or if ended less than 1 second ago
3118            conv.end_time.map(|end| now.duration_since(end).as_secs() < 1).unwrap_or(false)
3119        });
3120    }
3121
3122    /// Get or create the user conversation state
3123    pub async fn get_or_create_user_conversation_state(
3124        &self,
3125    ) -> Arc<std::sync::Mutex<crate::llm::ConversationState>> {
3126        let mut inner = self.inner.write().await;
3127
3128        if inner.user_conversation_state.is_none() {
3129            // Create with default 50k character limit for conversation history
3130            let mut conversation_state = crate::llm::ConversationState::new(50000);
3131
3132            // Sync documented protocols from AppState to ConversationState
3133            // This ensures the LLM knows which protocols have been documented
3134            // even when conversation history has been truncated
3135            if !inner.documented_server_protocols.is_empty() {
3136                let protocols: Vec<String> =
3137                    inner.documented_server_protocols.iter().cloned().collect();
3138                conversation_state.mark_server_protocols_documented(&protocols);
3139            }
3140            if !inner.documented_client_protocols.is_empty() {
3141                let protocols: Vec<String> =
3142                    inner.documented_client_protocols.iter().cloned().collect();
3143                conversation_state.mark_client_protocols_documented(&protocols);
3144            }
3145
3146            inner.user_conversation_state =
3147                Some(Arc::new(std::sync::Mutex::new(conversation_state)));
3148        } else {
3149            // Sync documented protocols if conversation state already exists
3150            // This ensures protocols documented in this session are reflected
3151            if let Some(ref conv_state) = inner.user_conversation_state {
3152                if let Ok(mut state) = conv_state.lock() {
3153                    // Only sync if AppState has more protocols than ConversationState
3154                    for protocol in &inner.documented_server_protocols {
3155                        if !state.documented_server_protocols.contains(protocol) {
3156                            state.mark_server_protocols_documented(std::slice::from_ref(protocol));
3157                        }
3158                    }
3159                    for protocol in &inner.documented_client_protocols {
3160                        if !state.documented_client_protocols.contains(protocol) {
3161                            state.mark_client_protocols_documented(std::slice::from_ref(protocol));
3162                        }
3163                    }
3164                }
3165            }
3166        }
3167
3168        inner.user_conversation_state.as_ref().unwrap().clone()
3169    }
3170
3171    /// Clear user conversation history
3172    pub async fn clear_user_conversation_history(&self) {
3173        let inner = self.inner.read().await;
3174        if let Some(conversation_state) = &inner.user_conversation_state {
3175            if let Ok(mut state) = conversation_state.lock() {
3176                state.clear_history();
3177            }
3178        }
3179    }
3180
3181    /// Get formatted conversation history for prompts
3182    pub async fn get_user_conversation_history(&self) -> Option<String> {
3183        let inner = self.inner.read().await;
3184        if let Some(conversation_state) = &inner.user_conversation_state {
3185            if let Ok(state) = conversation_state.lock() {
3186                if !state.is_empty() {
3187                    return Some(state.get_history_for_prompt());
3188                }
3189            }
3190        }
3191        None
3192    }
3193
3194    // ===== Access Log Methods =====
3195
3196    /// Record a request/response access-log entry (ring buffer, oldest dropped past capacity).
3197    /// Returns the assigned entry id.
3198    pub async fn record_access_log(
3199        &self,
3200        owner: AccessLogOwner,
3201        protocol: &str,
3202        connection_id: Option<u32>,
3203        event_type: &str,
3204        request: serde_json::Value,
3205        response: Vec<serde_json::Value>,
3206    ) -> u64 {
3207        let unix_ms = crate::utils::clock::SystemTime::now()
3208            .duration_since(crate::utils::clock::UNIX_EPOCH)
3209            .map(|d| d.as_millis() as u64)
3210            .unwrap_or(0);
3211        let (server_id, client_id) = match owner {
3212            AccessLogOwner::Server(id) => (Some(id), None),
3213            AccessLogOwner::Client(id) => (None, Some(id)),
3214        };
3215
3216        let mut inner = self.inner.write().await;
3217        let id = inner.next_access_log_id;
3218        inner.next_access_log_id += 1;
3219        inner.access_logs.push_back(AccessLogEntry {
3220            id,
3221            unix_ms,
3222            server_id,
3223            client_id,
3224            protocol: protocol.to_string(),
3225            connection_id,
3226            event_type: event_type.to_string(),
3227            request,
3228            response,
3229        });
3230        while inner.access_logs.len() > ACCESS_LOG_CAPACITY {
3231            inner.access_logs.pop_front();
3232        }
3233        id
3234    }
3235
3236    /// How many access-log entries have ever been recorded — one per network event a server
3237    /// or client handled (whatever answered it: handler, model or operator), plus one per
3238    /// injected action. Unlike the ring buffer this never shrinks, so it is a monotonic count
3239    /// of handled events for the life of the process (`--exit-after-events` reads it).
3240    pub async fn access_log_total(&self) -> u64 {
3241        self.inner.read().await.next_access_log_id.saturating_sub(1)
3242    }
3243
3244    /// Return the most recent access-log entries, newest first (up to `limit`).
3245    /// Pass `None` for the full retained buffer.
3246    pub async fn list_access_logs(&self, limit: Option<usize>) -> Vec<AccessLogEntry> {
3247        self.list_access_logs_for(None, limit).await
3248    }
3249
3250    /// Return the most recent access-log entries for one instance (or all,
3251    /// with `owner: None`), newest first (up to `limit`).
3252    pub async fn list_access_logs_for(
3253        &self,
3254        owner: Option<AccessLogOwner>,
3255        limit: Option<usize>,
3256    ) -> Vec<AccessLogEntry> {
3257        let inner = self.inner.read().await;
3258        let take = limit.unwrap_or(ACCESS_LOG_CAPACITY);
3259        inner
3260            .access_logs
3261            .iter()
3262            .rev()
3263            .filter(|entry| owner.is_none() || entry.owner() == owner)
3264            .take(take)
3265            .cloned()
3266            .collect()
3267    }
3268
3269    /// Look up a single access-log entry by id.
3270    pub async fn get_access_log(&self, id: u64) -> Option<AccessLogEntry> {
3271        let inner = self.inner.read().await;
3272        inner.access_logs.iter().find(|e| e.id == id).cloned()
3273    }
3274
3275    // ===== LLM Token Tracking Methods =====
3276
3277    /// Record LLM call tokens
3278    pub async fn record_llm_tokens(&self, input_tokens: u64, output_tokens: u64) {
3279        let mut inner = self.inner.write().await;
3280        inner.total_input_tokens += input_tokens;
3281        inner.total_output_tokens += output_tokens;
3282        inner.total_llm_calls += 1;
3283    }
3284
3285    /// Get LLM token statistics
3286    pub async fn get_llm_stats(&self) -> (u64, u64, u64) {
3287        let inner = self.inner.read().await;
3288        (
3289            inner.total_input_tokens,
3290            inner.total_output_tokens,
3291            inner.total_llm_calls,
3292        )
3293    }
3294
3295    /// Reset LLM token statistics
3296    pub async fn reset_llm_stats(&self) {
3297        let mut inner = self.inner.write().await;
3298        inner.total_input_tokens = 0;
3299        inner.total_output_tokens = 0;
3300        inner.total_llm_calls = 0;
3301    }
3302
3303    // ===== Database Management =====
3304
3305    /// Create a new database
3306    #[cfg(feature = "sqlite")]
3307    pub async fn create_database(
3308        &self,
3309        name: String,
3310        path: String,
3311        owner: crate::state::DatabaseOwner,
3312        init_sql: Option<&str>,
3313    ) -> anyhow::Result<crate::state::DatabaseId> {
3314        use anyhow::Context;
3315
3316        let mut inner = self.inner.write().await;
3317        let id = crate::state::DatabaseId::new(inner.next_unified_id);
3318        inner.next_unified_id += 1;
3319
3320        inner
3321            ._database_manager
3322            .create_database(id, name, path, owner, init_sql)
3323            .context("Failed to create database")?;
3324
3325        Ok(id)
3326    }
3327
3328    /// Execute a SQL query on a database
3329    #[cfg(feature = "sqlite")]
3330    pub async fn execute_sql(
3331        &self,
3332        db_id: crate::state::DatabaseId,
3333        sql: &str,
3334    ) -> anyhow::Result<crate::state::QueryResult> {
3335        let mut inner = self.inner.write().await;
3336        inner._database_manager.execute_query(db_id, sql)
3337    }
3338
3339    /// Get all databases
3340    #[cfg(feature = "sqlite")]
3341    pub async fn get_all_databases(&self) -> Vec<crate::state::DatabaseInstance> {
3342        let inner = self.inner.read().await;
3343        inner
3344            ._database_manager
3345            .get_all_instances()
3346            .into_iter()
3347            .cloned()
3348            .collect()
3349    }
3350
3351    /// Get a database by ID
3352    #[cfg(feature = "sqlite")]
3353    pub async fn get_database(
3354        &self,
3355        db_id: crate::state::DatabaseId,
3356    ) -> Option<crate::state::DatabaseInstance> {
3357        let inner = self.inner.read().await;
3358        inner._database_manager.get_instance(db_id).cloned()
3359    }
3360
3361    /// Delete a database
3362    #[cfg(feature = "sqlite")]
3363    pub async fn delete_database(&self, db_id: crate::state::DatabaseId) -> anyhow::Result<()> {
3364        let mut inner = self.inner.write().await;
3365        inner._database_manager.delete_database(db_id)
3366    }
3367
3368    /// Get databases owned by a server
3369    #[cfg(feature = "sqlite")]
3370    pub async fn get_databases_by_server(
3371        &self,
3372        server_id: crate::state::ServerId,
3373    ) -> Vec<crate::state::DatabaseInstance> {
3374        let inner = self.inner.read().await;
3375        inner
3376            ._database_manager
3377            .get_databases_by_server(server_id)
3378            .into_iter()
3379            .cloned()
3380            .collect()
3381    }
3382
3383    /// Get databases owned by a client
3384    #[cfg(feature = "sqlite")]
3385    pub async fn get_databases_by_client(
3386        &self,
3387        client_id: crate::state::ClientId,
3388    ) -> Vec<crate::state::DatabaseInstance> {
3389        let inner = self.inner.read().await;
3390        inner
3391            ._database_manager
3392            .get_databases_by_client(client_id)
3393            .into_iter()
3394            .cloned()
3395            .collect()
3396    }
3397
3398    /// Delete all databases owned by a server (called when server closes)
3399    #[cfg(feature = "sqlite")]
3400    pub async fn cleanup_databases_for_server(
3401        &self,
3402        server_id: crate::state::ServerId,
3403    ) -> anyhow::Result<()> {
3404        let mut inner = self.inner.write().await;
3405        inner
3406            ._database_manager
3407            .delete_databases_by_server(server_id)
3408    }
3409
3410    /// Delete all databases owned by a client (called when client disconnects)
3411    #[cfg(feature = "sqlite")]
3412    pub async fn cleanup_databases_for_client(
3413        &self,
3414        client_id: crate::state::ClientId,
3415    ) -> anyhow::Result<()> {
3416        let mut inner = self.inner.write().await;
3417        inner
3418            ._database_manager
3419            .delete_databases_by_client(client_id)
3420    }
3421
3422    /// Test-only helper to add a server with a specific ID (bypasses auto-increment)
3423    ///
3424    /// WARNING: This method is intended for testing only. In production code, use `add_server()`
3425    /// which properly assigns server IDs.
3426    pub async fn add_server_with_id(&self, server: ServerInstance) {
3427        let mut inner = self.inner.write().await;
3428        inner.servers.insert(server.id, server);
3429
3430        // Set mode to Server if this is the first server
3431        if inner.mode == Mode::Idle {
3432            inner.mode = Mode::Server;
3433        }
3434    }
3435
3436    /// Store Tor client instance for directory queries
3437    #[cfg(unix)]
3438    pub async fn set_tor_client(
3439        &self,
3440        client_id: ClientId,
3441        tor_client: Arc<arti_client::TorClient<tor_rtcompat::PreferredRuntime>>,
3442    ) {
3443        let mut inner = self.inner.write().await;
3444        inner.tor_clients.insert(client_id, tor_client);
3445    }
3446
3447    /// Get Tor client instance by client ID
3448    #[cfg(unix)]
3449    pub async fn get_tor_client(
3450        &self,
3451        client_id: ClientId,
3452    ) -> Option<Arc<arti_client::TorClient<tor_rtcompat::PreferredRuntime>>> {
3453        let inner = self.inner.read().await;
3454        inner.tor_clients.get(&client_id).cloned()
3455    }
3456
3457    /// Remove Tor client instance (called when client closes)
3458    #[cfg(unix)]
3459    pub async fn remove_tor_client(&self, client_id: ClientId) {
3460        let mut inner = self.inner.write().await;
3461        inner.tor_clients.remove(&client_id);
3462    }
3463}
3464
3465impl Default for AppState {
3466    fn default() -> Self {
3467        Self::new()
3468    }
3469}