1use 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::server::{LeanCtxServer, SessionMode};
11use super::startup::detect_startup_context;
12
13impl Default for LeanCtxServer {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl LeanCtxServer {
20 pub fn new() -> Self {
22 Self::new_with_project_root(None)
23 }
24
25 pub fn new_with_project_root(project_root: Option<&str>) -> Self {
27 Self::new_with_startup(
28 project_root,
29 std::env::current_dir().ok().as_deref(),
30 SessionMode::Personal,
31 "default",
32 "default",
33 )
34 }
35
36 pub fn new_shared_with_context(
38 project_root: &str,
39 workspace_id: &str,
40 channel_id: &str,
41 ) -> Self {
42 Self::new_with_startup(
43 Some(project_root),
44 std::env::current_dir().ok().as_deref(),
45 SessionMode::Shared,
46 workspace_id,
47 channel_id,
48 )
49 }
50
51 pub(crate) fn new_with_startup(
52 project_root: Option<&str>,
53 startup_cwd: Option<&Path>,
54 session_mode: SessionMode,
55 workspace_id: &str,
56 channel_id: &str,
57 ) -> Self {
58 let ttl = std::env::var("LEAN_CTX_CACHE_TTL")
59 .ok()
60 .and_then(|v| v.parse().ok())
61 .unwrap_or_else(|| {
62 let cfg = crate::core::config::Config::load();
63 crate::core::config::MemoryCleanup::effective(&cfg).idle_ttl_secs()
64 });
65
66 crate::core::graph_index::ProjectIndex::purge_stale_indices();
68
69 let startup = detect_startup_context(project_root, startup_cwd);
70 let (session, context_os) = match session_mode {
71 SessionMode::Personal => {
72 let mut session = SessionState::new();
76 if let Some(ref root) = startup.project_root {
77 session.project_root = Some(root.clone());
78 }
79 if let Some(ref cwd) = startup.shell_cwd {
80 session.shell_cwd = Some(cwd.clone());
81 }
82 (Arc::new(RwLock::new(session)), None)
83 }
84 SessionMode::Shared => {
85 let Some(ref root) = startup.project_root else {
86 return Self::new_with_startup(
88 project_root,
89 startup_cwd,
90 SessionMode::Personal,
91 workspace_id,
92 channel_id,
93 );
94 };
95 let rt = crate::core::context_os::runtime();
96 let session = rt
97 .shared_sessions
98 .get_or_load(root, workspace_id, channel_id);
99 rt.metrics.record_session_loaded();
100 if let Some(ref cwd) = startup.shell_cwd
102 && let Ok(mut s) = session.try_write()
103 {
104 s.shell_cwd = Some(cwd.clone());
105 }
106 (session, Some(rt))
107 }
108 };
109
110 crate::core::read_stub_index::load();
123
124 let cache = Arc::new(RwLock::new(SessionCache::new()));
125 let bm25_cache: Arc<std::sync::Mutex<Option<crate::core::bm25_cache::Bm25CacheEntry>>> =
126 Arc::new(std::sync::Mutex::new(None));
127
128 let eviction_target = std::sync::Arc::new(
131 crate::core::eviction_orchestrator::EvictionOrchestrator::new(
132 cache.clone(),
133 bm25_cache.clone(),
134 ),
135 );
136 crate::core::eviction_orchestrator::register(&eviction_target);
137 crate::core::memory_guard::start_guard(std::sync::Arc::new(
138 crate::core::eviction_orchestrator::on_memory_pressure,
139 ));
140
141 let presence_root = startup
142 .project_root
143 .as_deref()
144 .or(startup.shell_cwd.as_deref())
145 .unwrap_or(".");
146 let presence_agent_id =
147 match crate::core::agents::AgentRegistry::register_mcp_process(presence_root) {
148 Ok(agent_id) => Some(agent_id),
149 Err(error) => {
150 tracing::warn!("lean-ctx: failed to register MCP agent presence: {error}");
151 None
152 }
153 };
154
155 Self {
156 cache,
157 session,
158 tool_calls: Arc::new(RwLock::new(Vec::new())),
159 call_count: Arc::new(AtomicUsize::new(0)),
160 cache_ttl_secs: ttl,
161 last_call: Arc::new(RwLock::new(Instant::now())),
162 agent_id: Arc::new(RwLock::new(None)),
163 presence_agent_id: Arc::new(RwLock::new(presence_agent_id)),
164 client_name: Arc::new(RwLock::new(String::new())),
165 autonomy: Arc::new(crate::core::autonomy::AutonomyState::new()),
166 loop_detector: Arc::new(RwLock::new(
167 crate::core::loop_detection::LoopDetector::with_config(
168 &crate::core::config::Config::load().loop_detection,
169 ),
170 )),
171 workflow: Arc::new(RwLock::new(
172 crate::core::workflow::load_active().ok().flatten(),
173 )),
174 ledger: Arc::new(RwLock::new(
175 crate::core::context_ledger::ContextLedger::load(),
176 )),
177 pipeline_stats: Arc::new(RwLock::new(crate::core::pipeline::PipelineStats::new())),
178 session_mode,
179 workspace_id: if workspace_id.trim().is_empty() {
180 "default".to_string()
181 } else {
182 workspace_id.trim().to_string()
183 },
184 channel_id: if channel_id.trim().is_empty() {
185 "default".to_string()
186 } else {
187 channel_id.trim().to_string()
188 },
189 context_os,
190 context_ir: Some(std::sync::Arc::new(tokio::sync::RwLock::new(
191 crate::core::context_ir::ContextIrV1::load(),
192 ))),
193 registry: Some(std::sync::Arc::new(
194 crate::server::registry::build_registry(),
195 )),
196 rules_stale_checked: Arc::new(std::sync::atomic::AtomicBool::new(false)),
197 rules_tip_shown: Arc::new(std::sync::atomic::AtomicBool::new(false)),
198 last_seen_event_id: Arc::new(std::sync::atomic::AtomicI64::new(0)),
199 startup_project_root: startup.project_root,
200 startup_shell_cwd: startup.shell_cwd,
201 peer: Arc::new(tokio::sync::RwLock::new(None)),
202 has_client_roots: Arc::new(std::sync::atomic::AtomicBool::new(false)),
203 roots_resolved: Arc::new(std::sync::atomic::AtomicBool::new(false)),
204 roots_list_attempts: Arc::new(std::sync::atomic::AtomicU32::new(0)),
205 bm25_cache,
206 progress_sender: Arc::new(std::sync::Mutex::new(None)),
207 _eviction_target: eviction_target,
208 last_tools_config_hash: Arc::new(std::sync::atomic::AtomicU64::new(
209 crate::server::tools_config_watch::current_hash(),
210 )),
211 }
212 }
213
214 pub async fn check_idle_expiry(&self) {
216 if self.cache_ttl_secs == 0 {
217 return;
218 }
219 let last = *self.last_call.read().await;
220 if last.elapsed().as_secs() >= self.cache_ttl_secs {
221 {
222 let mut session = self.session.write().await;
223 let _ = session.save();
224 }
225 let mut cache = self.cache.write().await;
226 let redelivered = cache.count_full_delivered();
227 let count = cache.clear();
228 crate::core::cache_telemetry::record_idle(redelivered as u64);
229 crate::core::read_stub_index::persist();
233 if count > 0 {
234 tracing::info!(
235 "Cache auto-cleared after {}s idle ({count} file(s), {redelivered} forced re-delivery)",
236 self.cache_ttl_secs
237 );
238 }
239 }
240 *self.last_call.write().await = Instant::now();
241 }
242
243 async fn record_shutdown_episode(&self) {
244 let tool_calls: Vec<(String, u64)> = self
245 .tool_calls
246 .read()
247 .await
248 .iter()
249 .map(|call| (call.tool.clone(), call.duration_ms))
250 .collect();
251 if tool_calls.is_empty() {
252 return;
253 }
254
255 let session = self.session.read().await.clone();
256 let Some(project_root) = session
257 .project_root
258 .clone()
259 .or_else(|| self.startup_project_root.clone())
260 else {
261 return;
262 };
263 let agent_id = self.agent_id.read().await.clone();
264 let agent_id = match agent_id {
265 Some(agent_id) => Some(agent_id),
266 None => self.presence_agent_id.read().await.clone(),
267 };
268 let policy = crate::core::config::Config::load()
269 .memory_policy_effective()
270 .unwrap_or_default();
271 let project_hash = crate::core::project_hash::hash_project_root(&project_root);
272
273 match crate::core::episodic_memory::record_session_episode(
274 &project_hash,
275 &session,
276 &tool_calls,
277 agent_id.as_deref(),
278 &policy.episodic,
279 true,
280 ) {
281 Ok(Some(id)) => tracing::info!("lean-ctx: recorded shutdown episode {id}"),
282 Ok(None) => {}
283 Err(error) => tracing::warn!("lean-ctx: failed to record shutdown episode: {error}"),
284 }
285 }
286
287 pub async fn shutdown(&self) {
289 self.record_shutdown_episode().await;
290 if let Some(agent_id) = self.presence_agent_id.read().await.clone()
291 && let Err(error) = crate::core::agents::AgentRegistry::finish_persistent(&agent_id)
292 {
293 tracing::warn!("lean-ctx: failed to finish MCP agent presence: {error}");
294 }
295 {
296 let session = self.session.read().await;
297 let has_insights = !session.findings.is_empty() || !session.decisions.is_empty();
298 let root = session.project_root.clone();
299 drop(session);
300
301 if has_insights && let Some(ref root) = root {
302 crate::tools::startup::auto_consolidate_knowledge(root);
303 }
304 }
305 {
306 let mut session = self.session.write().await;
307 let _ = session.save();
308 }
309 crate::core::stats::flush();
316 crate::core::read_stub_index::persist();
319 {
320 let mut cache = self.cache.write().await;
321 let count = cache.clear();
322 if count > 0 {
323 tracing::info!("[shutdown] cleared {count} cached file(s)");
324 }
325 }
326 crate::core::memory_guard::force_purge();
327 }
328}