lean_ctx/tools/
server_lifecycle.rs1use std::path::Path;
2use std::sync::Arc;
3use std::sync::atomic::AtomicUsize;
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 pub fn new() -> Self {
23 Self::new_with_project_root(None)
24 }
25
26 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 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 crate::core::graph_index::ProjectIndex::purge_stale_indices();
69
70 let startup = detect_startup_context(project_root, startup_cwd);
71 let (session, context_os) = match session_mode {
72 SessionMode::Personal => {
73 let mut session = if let Some(ref root) = startup.project_root {
74 SessionState::load_latest_for_project_root(root).unwrap_or_default()
75 } else {
76 SessionState::load_latest().unwrap_or_default()
77 };
78 if let Some(ref root) = startup.project_root {
79 session.project_root = Some(root.clone());
80 }
81 if let Some(ref cwd) = startup.shell_cwd {
82 session.shell_cwd = Some(cwd.clone());
83 }
84 (Arc::new(RwLock::new(session)), None)
85 }
86 SessionMode::Shared => {
87 let Some(ref root) = startup.project_root else {
88 return Self::new_with_startup(
90 project_root,
91 startup_cwd,
92 SessionMode::Personal,
93 workspace_id,
94 channel_id,
95 );
96 };
97 let rt = crate::core::context_os::runtime();
98 let session = rt
99 .shared_sessions
100 .get_or_load(root, workspace_id, channel_id);
101 rt.metrics.record_session_loaded();
102 if let Some(ref cwd) = startup.shell_cwd
104 && let Ok(mut s) = session.try_write()
105 {
106 s.shell_cwd = Some(cwd.clone());
107 }
108 (session, Some(rt))
109 }
110 };
111
112 crate::core::read_stub_index::load();
125
126 let cache = Arc::new(RwLock::new(SessionCache::new()));
127 let bm25_cache: Arc<std::sync::Mutex<Option<crate::core::bm25_cache::Bm25CacheEntry>>> =
128 Arc::new(std::sync::Mutex::new(None));
129
130 let eviction_target = std::sync::Arc::new(
133 crate::core::eviction_orchestrator::EvictionOrchestrator::new(
134 cache.clone(),
135 bm25_cache.clone(),
136 ),
137 );
138 crate::core::eviction_orchestrator::register(&eviction_target);
139 crate::core::memory_guard::start_guard(std::sync::Arc::new(
140 crate::core::eviction_orchestrator::on_memory_pressure,
141 ));
142
143 let presence_root = startup
144 .project_root
145 .as_deref()
146 .or(startup.shell_cwd.as_deref())
147 .unwrap_or(".");
148 let presence_agent_id =
149 match crate::core::agents::AgentRegistry::register_mcp_process(presence_root) {
150 Ok(agent_id) => Some(agent_id),
151 Err(error) => {
152 tracing::warn!("lean-ctx: failed to register MCP agent presence: {error}");
153 None
154 }
155 };
156
157 Self {
158 cache,
159 session,
160 tool_calls: Arc::new(RwLock::new(Vec::new())),
161 call_count: Arc::new(AtomicUsize::new(0)),
162 cache_ttl_secs: ttl,
163 last_call: Arc::new(RwLock::new(Instant::now())),
164 agent_id: Arc::new(RwLock::new(None)),
165 presence_agent_id: Arc::new(RwLock::new(presence_agent_id)),
166 client_name: Arc::new(RwLock::new(String::new())),
167 autonomy: Arc::new(autonomy::AutonomyState::new()),
168 loop_detector: Arc::new(RwLock::new(
169 crate::core::loop_detection::LoopDetector::with_config(
170 &crate::core::config::Config::load().loop_detection,
171 ),
172 )),
173 workflow: Arc::new(RwLock::new(
174 crate::core::workflow::load_active().ok().flatten(),
175 )),
176 ledger: Arc::new(RwLock::new(
177 crate::core::context_ledger::ContextLedger::load(),
178 )),
179 pipeline_stats: Arc::new(RwLock::new(crate::core::pipeline::PipelineStats::new())),
180 session_mode,
181 workspace_id: if workspace_id.trim().is_empty() {
182 "default".to_string()
183 } else {
184 workspace_id.trim().to_string()
185 },
186 channel_id: if channel_id.trim().is_empty() {
187 "default".to_string()
188 } else {
189 channel_id.trim().to_string()
190 },
191 context_os,
192 context_ir: Some(std::sync::Arc::new(tokio::sync::RwLock::new(
193 crate::core::context_ir::ContextIrV1::load(),
194 ))),
195 registry: Some(std::sync::Arc::new(
196 crate::server::registry::build_registry(),
197 )),
198 rules_stale_checked: Arc::new(std::sync::atomic::AtomicBool::new(false)),
199 rules_tip_shown: Arc::new(std::sync::atomic::AtomicBool::new(false)),
200 last_seen_event_id: Arc::new(std::sync::atomic::AtomicI64::new(0)),
201 startup_project_root: startup.project_root,
202 startup_shell_cwd: startup.shell_cwd,
203 peer: Arc::new(tokio::sync::RwLock::new(None)),
204 has_client_roots: Arc::new(std::sync::atomic::AtomicBool::new(false)),
205 roots_resolved: Arc::new(std::sync::atomic::AtomicBool::new(false)),
206 roots_list_attempts: Arc::new(std::sync::atomic::AtomicU32::new(0)),
207 bm25_cache,
208 progress_sender: Arc::new(std::sync::Mutex::new(None)),
209 _eviction_target: eviction_target,
210 last_tools_config_hash: Arc::new(std::sync::atomic::AtomicU64::new(
211 crate::server::tools_config_watch::current_hash(),
212 )),
213 }
214 }
215
216 pub async fn check_idle_expiry(&self) {
218 if self.cache_ttl_secs == 0 {
219 return;
220 }
221 let last = *self.last_call.read().await;
222 if last.elapsed().as_secs() >= self.cache_ttl_secs {
223 {
224 let mut session = self.session.write().await;
225 let _ = session.save();
226 }
227 let mut cache = self.cache.write().await;
228 let redelivered = cache.count_full_delivered();
229 let count = cache.clear();
230 crate::core::cache_telemetry::record_idle(redelivered as u64);
231 crate::core::read_stub_index::persist();
235 if count > 0 {
236 tracing::info!(
237 "Cache auto-cleared after {}s idle ({count} file(s), {redelivered} forced re-delivery)",
238 self.cache_ttl_secs
239 );
240 }
241 }
242 *self.last_call.write().await = Instant::now();
243 }
244
245 pub async fn shutdown(&self) {
247 if let Some(agent_id) = self.presence_agent_id.read().await.clone()
248 && let Err(error) = crate::core::agents::AgentRegistry::finish_persistent(&agent_id)
249 {
250 tracing::warn!("lean-ctx: failed to finish MCP agent presence: {error}");
251 }
252 {
253 let session = self.session.read().await;
254 let has_insights = !session.findings.is_empty() || !session.decisions.is_empty();
255 let root = session.project_root.clone();
256 drop(session);
257
258 if has_insights && let Some(ref root) = root {
259 crate::tools::startup::auto_consolidate_knowledge(root);
260 }
261 }
262 {
263 let mut session = self.session.write().await;
264 let _ = session.save();
265 }
266 crate::core::stats::flush();
273 crate::core::read_stub_index::persist();
276 {
277 let mut cache = self.cache.write().await;
278 let count = cache.clear();
279 if count > 0 {
280 tracing::info!("[shutdown] cleared {count} cached file(s)");
281 }
282 }
283 crate::core::memory_guard::force_purge();
284 }
285}