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