use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
static IN_FLIGHT: AtomicUsize = AtomicUsize::new(0);
static LAST_ACTIVITY_MS: AtomicU64 = AtomicU64::new(0);
static SERVING: AtomicBool = AtomicBool::new(false);
const POLL: Duration = Duration::from_secs(30);
const DEFERRED_CHECKS_PER_POLL: usize = 4;
fn base() -> Instant {
static BASE: OnceLock<Instant> = OnceLock::new();
*BASE.get_or_init(Instant::now)
}
fn now_ms() -> u64 {
u64::try_from(base().elapsed().as_millis()).unwrap_or(u64::MAX)
}
pub(crate) struct CallActivity(());
pub(crate) fn begin_call() -> CallActivity {
IN_FLIGHT.fetch_add(1, Ordering::SeqCst);
LAST_ACTIVITY_MS.store(now_ms(), Ordering::SeqCst);
CallActivity(())
}
impl Drop for CallActivity {
fn drop(&mut self) {
LAST_ACTIVITY_MS.store(now_ms(), Ordering::SeqCst);
IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
}
}
pub(crate) fn idle_for_now() -> Option<Duration> {
if !SERVING.load(Ordering::SeqCst) {
return None;
}
idle_for(
now_ms(),
LAST_ACTIVITY_MS.load(Ordering::SeqCst),
IN_FLIGHT.load(Ordering::SeqCst),
0,
)
}
fn idle_for(now_ms: u64, last_ms: u64, in_flight: usize, running_jobs: usize) -> Option<Duration> {
(in_flight == 0 && running_jobs == 0)
.then(|| Duration::from_millis(now_ms.saturating_sub(last_ms)))
}
#[derive(Debug, PartialEq, Eq)]
enum IdleStep {
Wait,
Release,
Exit,
}
fn decide(
idle: Option<Duration>,
released: bool,
release_after: Duration,
exit_after: Duration,
) -> IdleStep {
let Some(idle) = idle else {
return IdleStep::Wait;
};
if !exit_after.is_zero() && idle >= exit_after {
IdleStep::Exit
} else if !released && !release_after.is_zero() && idle >= release_after {
IdleStep::Release
} else {
IdleStep::Wait
}
}
pub fn spawn(server: crate::tools::LeanCtxServer) {
let exit_after = Duration::from_secs(
lean_ctx::core::config::Config::load()
.mcp_idle_exit_minutes_effective()
.saturating_mul(60),
);
let release_after = server.idle_ttl();
LAST_ACTIVITY_MS.store(now_ms(), Ordering::SeqCst);
SERVING.store(true, Ordering::SeqCst);
lean_ctx_hive::tasks::register_idle_clock(idle_for_now);
tokio::spawn(async move {
let mut released_stretch: Option<u64> = None;
loop {
tokio::time::sleep(POLL).await;
let last = LAST_ACTIVITY_MS.load(Ordering::SeqCst);
let idle = idle_for(
now_ms(),
last,
IN_FLIGHT.load(Ordering::SeqCst),
crate::server::background_shell::running_count(),
);
match decide(
idle,
released_stretch == Some(last),
release_after,
exit_after,
) {
IdleStep::Wait => {
if idle.is_some()
&& lean_ctx::core::context_admission::semantic_deferred::pending()
{
tokio::task::spawn_blocking(|| {
lean_ctx::core::context_admission::semantic_deferred::drain(
DEFERRED_CHECKS_PER_POLL,
|| IN_FLIGHT.load(Ordering::SeqCst) == 0,
)
});
}
}
IdleStep::Release => {
tracing::info!(
"[mcp-idle] no tool call for {}s — releasing caches and indexes",
release_after.as_secs()
);
server.release_idle_memory().await;
released_stretch = Some(last);
}
IdleStep::Exit => {
tracing::info!(
"[mcp-idle] no tool call for {} min — exiting (mcp_idle_exit_minutes)",
exit_after.as_secs() / 60
);
let _ = tokio::task::spawn_blocking(|| {
lean_ctx::core::tool_lifecycle::flush_all_at_exit();
lean_ctx::core::telemetry_features::record_mcp_session_end();
lean_ctx::core::telemetry_send::send_telemetry(
lean_ctx::core::telemetry_aggregate::SendTrigger::Exit,
);
})
.await;
std::process::exit(0);
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
const MIN: Duration = Duration::from_mins(1);
#[test]
fn busy_server_is_never_idle() {
assert_eq!(idle_for(10_000, 0, 1, 0), None);
assert_eq!(idle_for(10_000, 0, 0, 2), None);
assert_eq!(idle_for(10_000, 4_000, 0, 0), Some(Duration::from_secs(6)));
assert_eq!(idle_for(0, 5, 0, 0), Some(Duration::ZERO));
}
#[test]
fn release_once_per_idle_stretch_then_exit_only_when_enabled() {
let release = 60 * MIN;
assert_eq!(decide(None, false, release, 5 * MIN), IdleStep::Wait);
assert_eq!(
decide(Some(59 * MIN), false, release, Duration::ZERO),
IdleStep::Wait
);
assert_eq!(
decide(Some(60 * MIN), false, release, Duration::ZERO),
IdleStep::Release
);
assert_eq!(
decide(Some(90 * MIN), true, release, Duration::ZERO),
IdleStep::Wait
);
assert_eq!(
decide(Some(10_000 * MIN), true, release, Duration::ZERO),
IdleStep::Wait
);
assert_eq!(
decide(Some(120 * MIN), true, release, 120 * MIN),
IdleStep::Exit
);
assert_eq!(
decide(Some(5 * MIN), false, Duration::ZERO, Duration::ZERO),
IdleStep::Wait
);
}
}