Skip to main content

lean_ctx/tools/
server_lifecycle.rs

1use std::path::Path;
2use std::sync::atomic::AtomicUsize;
3use std::sync::Arc;
4use std::time::Instant;
5use tokio::sync::RwLock;
6
7use crate::core::cache::SessionCache;
8use crate::core::session::SessionState;
9
10use super::autonomy;
11use super::server::{LeanCtxServer, SessionMode};
12use super::startup::detect_startup_context;
13
14impl Default for LeanCtxServer {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl LeanCtxServer {
21    /// Creates a new server with default settings, auto-detecting the project root.
22    pub fn new() -> Self {
23        Self::new_with_project_root(None)
24    }
25
26    /// Creates a new server rooted at the given project directory.
27    pub fn new_with_project_root(project_root: Option<&str>) -> Self {
28        Self::new_with_startup(
29            project_root,
30            std::env::current_dir().ok().as_deref(),
31            SessionMode::Personal,
32            "default",
33            "default",
34        )
35    }
36
37    /// Creates a new server in Context OS shared mode for a specific workspace/channel.
38    pub fn new_shared_with_context(
39        project_root: &str,
40        workspace_id: &str,
41        channel_id: &str,
42    ) -> Self {
43        Self::new_with_startup(
44            Some(project_root),
45            std::env::current_dir().ok().as_deref(),
46            SessionMode::Shared,
47            workspace_id,
48            channel_id,
49        )
50    }
51
52    pub(crate) fn new_with_startup(
53        project_root: Option<&str>,
54        startup_cwd: Option<&Path>,
55        session_mode: SessionMode,
56        workspace_id: &str,
57        channel_id: &str,
58    ) -> Self {
59        let ttl = std::env::var("LEAN_CTX_CACHE_TTL")
60            .ok()
61            .and_then(|v| v.parse().ok())
62            .unwrap_or_else(|| {
63                let cfg = crate::core::config::Config::load();
64                crate::core::config::MemoryCleanup::effective(&cfg).idle_ttl_secs()
65            });
66
67        // Purge stale graph indices on startup to prevent serving outdated data
68        crate::core::graph_index::ProjectIndex::purge_stale_indices();
69
70        // Start the RAM guardian — monitors RSS and triggers tiered eviction.
71        // At Critical pressure (>3x limit), performs emergency shutdown.
72        crate::core::memory_guard::start_guard(std::sync::Arc::new(|level| {
73            use crate::core::memory_guard::PressureLevel;
74            match level {
75                PressureLevel::Soft => {
76                    tracing::info!("[memory_guard] soft pressure — deferring new index builds");
77                }
78                PressureLevel::Medium | PressureLevel::Hard | PressureLevel::Critical => {
79                    tracing::warn!(
80                        "[memory_guard] {:?} pressure — purging jemalloc arenas",
81                        level,
82                    );
83                    crate::core::memory_guard::jemalloc_purge();
84                }
85                PressureLevel::Normal => {}
86            }
87        }));
88
89        let startup = detect_startup_context(project_root, startup_cwd);
90        let (session, context_os) = match session_mode {
91            SessionMode::Personal => {
92                let mut session = if let Some(ref root) = startup.project_root {
93                    SessionState::load_latest_for_project_root(root).unwrap_or_default()
94                } else {
95                    SessionState::load_latest().unwrap_or_default()
96                };
97                if let Some(ref root) = startup.project_root {
98                    session.project_root = Some(root.clone());
99                }
100                if let Some(ref cwd) = startup.shell_cwd {
101                    session.shell_cwd = Some(cwd.clone());
102                }
103                (Arc::new(RwLock::new(session)), None)
104            }
105            SessionMode::Shared => {
106                let Some(ref root) = startup.project_root else {
107                    // Shared mode without a project root is not useful; fall back to personal.
108                    return Self::new_with_startup(
109                        project_root,
110                        startup_cwd,
111                        SessionMode::Personal,
112                        workspace_id,
113                        channel_id,
114                    );
115                };
116                let rt = crate::core::context_os::runtime();
117                let session = rt
118                    .shared_sessions
119                    .get_or_load(root, workspace_id, channel_id);
120                rt.metrics.record_session_loaded();
121                // Ensure shell_cwd is refreshed (best-effort).
122                if let Some(ref cwd) = startup.shell_cwd {
123                    if let Ok(mut s) = session.try_write() {
124                        s.shell_cwd = Some(cwd.clone());
125                    }
126                }
127                (session, Some(rt))
128            }
129        };
130
131        Self {
132            cache: Arc::new(RwLock::new(SessionCache::new())),
133            session,
134            tool_calls: Arc::new(RwLock::new(Vec::new())),
135            call_count: Arc::new(AtomicUsize::new(0)),
136            cache_ttl_secs: ttl,
137            last_call: Arc::new(RwLock::new(Instant::now())),
138            agent_id: Arc::new(RwLock::new(None)),
139            client_name: Arc::new(RwLock::new(String::new())),
140            autonomy: Arc::new(autonomy::AutonomyState::new()),
141            loop_detector: Arc::new(RwLock::new(
142                crate::core::loop_detection::LoopDetector::with_config(
143                    &crate::core::config::Config::load().loop_detection,
144                ),
145            )),
146            workflow: Arc::new(RwLock::new(
147                crate::core::workflow::load_active().ok().flatten(),
148            )),
149            ledger: Arc::new(RwLock::new(
150                crate::core::context_ledger::ContextLedger::load(),
151            )),
152            pipeline_stats: Arc::new(RwLock::new(crate::core::pipeline::PipelineStats::new())),
153            session_mode,
154            workspace_id: if workspace_id.trim().is_empty() {
155                "default".to_string()
156            } else {
157                workspace_id.trim().to_string()
158            },
159            channel_id: if channel_id.trim().is_empty() {
160                "default".to_string()
161            } else {
162                channel_id.trim().to_string()
163            },
164            context_os,
165            context_ir: None,
166            registry: Some(std::sync::Arc::new(
167                crate::server::registry::build_registry(),
168            )),
169            rules_stale_checked: Arc::new(std::sync::atomic::AtomicBool::new(false)),
170            last_seen_event_id: Arc::new(std::sync::atomic::AtomicI64::new(0)),
171            startup_project_root: startup.project_root,
172            startup_shell_cwd: startup.shell_cwd,
173            peer: Arc::new(tokio::sync::RwLock::new(None)),
174        }
175    }
176
177    /// Clears the cache and saves the session if the TTL idle threshold has been exceeded.
178    pub async fn check_idle_expiry(&self) {
179        if self.cache_ttl_secs == 0 {
180            return;
181        }
182        let last = *self.last_call.read().await;
183        if last.elapsed().as_secs() >= self.cache_ttl_secs {
184            {
185                let mut session = self.session.write().await;
186                let _ = session.save();
187            }
188            let mut cache = self.cache.write().await;
189            let count = cache.clear();
190            if count > 0 {
191                tracing::info!(
192                    "Cache auto-cleared after {}s idle ({count} file(s))",
193                    self.cache_ttl_secs
194                );
195            }
196        }
197        *self.last_call.write().await = Instant::now();
198    }
199
200    /// Aggressive cleanup on connection drop: save session, clear all caches, purge allocator.
201    pub async fn shutdown(&self) {
202        {
203            let mut session = self.session.write().await;
204            let _ = session.save();
205        }
206        {
207            let mut cache = self.cache.write().await;
208            let count = cache.clear();
209            if count > 0 {
210                tracing::info!("[shutdown] cleared {count} cached file(s)");
211            }
212        }
213        crate::core::memory_guard::force_purge();
214    }
215}