use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;
use rmcp::service::{NotificationContext, RoleServer};
use tokio::sync::Mutex;
use tracing::{info, warn};
use super::{SharedBashState, WinxService};
use crate::runtime::{EmbeddedShellRuntime, ShellRuntime};
use crate::state::bash_state::generate_thread_id;
use crate::state::task_state::TaskRegistry;
use crate::types::{Initialize, InitializeType, ModeName};
pub(super) const MAX_SESSIONS: usize = 32;
#[derive(Default)]
pub(super) struct SessionRegistry {
pub(super) slots: HashMap<String, SharedBashState>,
pub(super) last_used: HashMap<String, Instant>,
pub(super) in_flight: HashMap<String, SessionPin>,
pub(super) last_active: Option<String>,
}
#[derive(Clone)]
pub(super) struct SessionPin {
count: Arc<AtomicUsize>,
}
impl Default for SessionPin {
fn default() -> Self {
Self { count: Arc::new(AtomicUsize::new(0)) }
}
}
impl SessionPin {
fn acquire(&self) -> SessionGuard {
self.count.fetch_add(1, Ordering::SeqCst);
SessionGuard { count: self.count.clone() }
}
fn is_pinned(&self) -> bool {
self.count.load(Ordering::SeqCst) > 0
}
}
pub(super) struct SessionGuard {
count: Arc<AtomicUsize>,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
self.count.fetch_sub(1, Ordering::SeqCst);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SessionIsolation {
Lenient,
Strict,
}
impl Default for WinxService {
fn default() -> Self {
Self::new()
}
}
impl WinxService {
pub fn new() -> Self {
Self::with_isolation(SessionIsolation::Lenient)
}
pub fn with_isolation(isolation: SessionIsolation) -> Self {
Self::with_runtime(isolation, Arc::new(EmbeddedShellRuntime))
}
pub fn with_runtime(isolation: SessionIsolation, shell_runtime: Arc<dyn ShellRuntime>) -> Self {
info!(?isolation, "Creating new WinxService instance");
Self {
sessions: Arc::new(Mutex::new(SessionRegistry::default())),
tasks: Arc::new(Mutex::new(TaskRegistry::default())),
root_bootstrap: Arc::new(Mutex::new(())),
shell_runtime,
mutations: super::mutations::MutationCoordinator::default(),
version: crate::build_info::display_version().to_string(),
isolation,
}
}
pub(super) async fn session_for(&self, thread_id: &str) -> (SharedBashState, SessionGuard) {
let thread_id = crate::types::normalize_thread_id(thread_id);
let mut registry = self.sessions.lock().await;
let key = if thread_id.is_empty() {
match self.isolation {
SessionIsolation::Lenient => {
registry.last_active.clone().unwrap_or_else(|| "default".to_string())
}
SessionIsolation::Strict => "anonymous".to_string(),
}
} else {
thread_id.clone()
};
if !registry.slots.contains_key(&key) && registry.slots.len() >= MAX_SESSIONS {
let victim = registry
.last_used
.iter()
.filter(|(candidate, _)| **candidate != key)
.filter(|(candidate, _)| {
registry.in_flight.get(candidate.as_str()).is_none_or(|pin| !pin.is_pinned())
})
.min_by_key(|(_, last_used)| **last_used)
.map(|(candidate, _)| candidate.clone());
if let Some(victim) = victim {
if let Err(error) = self.shell_runtime.terminate_session(&victim).await {
warn!(%error, "failed to terminate LRU shell session '{victim}'");
}
registry.slots.remove(&victim);
registry.last_used.remove(&victim);
registry.in_flight.remove(&victim);
if registry.last_active.as_deref() == Some(victim.as_str()) {
registry.last_active = None;
}
warn!("Evicted LRU shell session '{victim}' (session cap {MAX_SESSIONS})");
} else {
warn!(
"All {MAX_SESSIONS} sessions busy; exceeding the cap rather than evicting an in-flight session"
);
}
}
let slot =
registry.slots.entry(key.clone()).or_insert_with(|| Arc::new(Mutex::new(None))).clone();
let pin = registry.in_flight.entry(key.clone()).or_default().clone();
registry.last_used.insert(key.clone(), Instant::now());
if !thread_id.is_empty() {
registry.last_active = Some(key);
}
(slot, pin.acquire())
}
pub(super) async fn active_slot(
&self,
session_prefix: Option<&str>,
) -> Option<SharedBashState> {
let registry = self.sessions.lock().await;
if let Some(prefix) = session_prefix {
return registry
.last_used
.iter()
.filter(|(key, _)| key.starts_with(prefix))
.max_by_key(|(_, last_used)| **last_used)
.and_then(|(key, _)| registry.slots.get(key).cloned());
}
registry.last_active.as_ref().and_then(|key| registry.slots.get(key).cloned())
}
pub(super) async fn bound_workspace(&self, thread_id: &str) -> Option<std::path::PathBuf> {
let thread_id = crate::types::normalize_thread_id(thread_id);
if thread_id.is_empty() {
return None;
}
let slot = {
let registry = self.sessions.lock().await;
registry.slots.get(&thread_id).cloned()
}?;
let state = slot.lock().await;
state.as_ref().filter(|state| state.initialized).map(|state| state.workspace_root.clone())
}
pub(super) async fn has_initialized_session(&self) -> bool {
let slots = {
let registry = self.sessions.lock().await;
registry.slots.values().cloned().collect::<Vec<_>>()
};
for slot in slots {
if slot.lock().await.as_ref().is_some_and(|state| state.initialized) {
return true;
}
}
false
}
#[allow(deprecated)]
pub(super) async fn initialize_from_client_roots(
&self,
context: NotificationContext<RoleServer>,
) {
if self.isolation != SessionIsolation::Lenient {
return;
}
let _bootstrap_guard = self.root_bootstrap.lock().await;
if self.has_initialized_session().await {
return;
}
let supports_roots =
context.peer.peer_info().is_some_and(|info| info.capabilities.roots.is_some());
if !supports_roots {
return;
}
let roots = match context.peer.list_roots().await {
Ok(result) => result.roots,
Err(error) => {
warn!(%error, "client advertised Roots but roots/list failed");
return;
}
};
let cwd = std::env::current_dir().ok();
let mut paths =
roots.iter().filter_map(|root| root_uri_to_path(&root.uri)).collect::<Vec<_>>();
let selected = cwd
.as_ref()
.and_then(|cwd| paths.iter().position(|path| cwd.starts_with(path)))
.map(|index| paths.remove(index))
.or_else(|| paths.into_iter().next());
let Some(workspace) = selected else {
warn!("client returned no usable local file Roots; Initialize remains required");
return;
};
let initialize = Initialize {
init_type: InitializeType::FirstCall,
any_workspace_path: workspace.to_string_lossy().into_owned(),
initial_files_to_read: Vec::new(),
task_id_to_resume: String::new(),
mode_name: ModeName::Wcgw,
thread_id: generate_thread_id(),
code_writer_config: None,
};
let arguments = match serde_json::to_value(initialize) {
Ok(arguments) => arguments,
Err(error) => {
warn!(%error, "failed to serialize Roots bootstrap request");
return;
}
};
match self.handle_initialize(Some(arguments)).await {
Ok(_) => info!(workspace = %workspace.display(), "initialized Winx from MCP Roots"),
Err(error) => warn!(%error.message, "failed to initialize Winx from MCP Roots"),
}
}
}
pub(super) fn root_uri_to_path(uri: &str) -> Option<std::path::PathBuf> {
let encoded = uri.strip_prefix("file://")?;
let encoded = if let Some(path) = encoded.strip_prefix("localhost/") {
format!("/{path}")
} else if encoded.starts_with('/') {
encoded.to_string()
} else {
return None;
};
let decoded = percent_encoding::percent_decode_str(&encoded).decode_utf8().ok()?;
let path = std::path::PathBuf::from(decoded.as_ref());
path.is_absolute().then_some(path)
}