use std::net::SocketAddr;
use std::sync::Arc;
use trusty_mpm::activity::monitor::{ActivityMonitor, OpenRouterClassifier};
use trusty_mpm::core::paths::FrameworkPaths;
use trusty_mpm::session_manager::real_tmux::NoopTmuxDriver;
use trusty_mpm::session_manager::{ManagedTmuxDriver, RealTmuxDriver, SessionManager};
use trusty_mpm::supervisor::config::{DEFAULT_LLM_MODEL, ENV_LLM_MODEL};
use trusty_mpm::supervisor::{Supervisor, SupervisorConfig};
pub(crate) fn apply_interval_override(
cfg: &mut SupervisorConfig,
interval: Option<u64>,
) -> anyhow::Result<()> {
match interval {
Some(0) => anyhow::bail!(
"--interval must be greater than 0 seconds (got 0); omit the flag to use the default"
),
Some(secs) => {
cfg.interval = std::time::Duration::from_secs(secs);
Ok(())
}
None => Ok(()),
}
}
pub(crate) async fn run_supervisor(
addr: Option<SocketAddr>,
interval: Option<u64>,
auto_resume: bool,
no_classify: bool,
) -> anyhow::Result<()> {
let mut cfg = SupervisorConfig::from_env();
if let Some(a) = addr {
cfg.metrics_addr = a;
}
apply_interval_override(&mut cfg, interval)?;
if auto_resume {
cfg.auto_resume = true;
}
if no_classify {
cfg.classify_idle = false;
}
let data_dir = FrameworkPaths::default().root.join("session-manager");
std::fs::create_dir_all(&data_dir)?;
let tmux: Arc<dyn ManagedTmuxDriver> = match RealTmuxDriver::discover() {
Ok(d) => Arc::new(d),
Err(e) => {
tracing::warn!("tmux unavailable for supervisor: {e}; using no-op driver");
Arc::new(NoopTmuxDriver)
}
};
let mgr = Arc::new(SessionManager::new(&data_dir, tmux).await?);
let monitor = if cfg.classify_idle {
let model = std::env::var(ENV_LLM_MODEL).unwrap_or_else(|_| DEFAULT_LLM_MODEL.to_owned());
Some(ActivityMonitor::new(OpenRouterClassifier::new(), model))
} else {
None
};
let handle = trusty_mpm::supervisor::new_handle();
let metrics_addr = cfg.metrics_addr;
let listener = trusty_mpm::supervisor::http::bind(metrics_addr).await?;
let server_handle = handle.clone();
let mut server_task = tokio::spawn(async move {
trusty_mpm::supervisor::http::serve_on(listener, server_handle).await
});
tracing::info!(
addr = %metrics_addr,
interval_secs = cfg.interval.as_secs(),
auto_resume = cfg.auto_resume,
classify_idle = cfg.classify_idle,
"starting unattended supervisor"
);
let supervisor = Supervisor::new(mgr, cfg, monitor);
tokio::select! {
loop_result = supervisor.run(handle) => loop_result,
server_result = &mut server_task => {
match server_result {
Ok(Ok(())) => {
anyhow::bail!("supervisor metrics server exited unexpectedly")
}
Ok(Err(e)) => Err(e.context("supervisor metrics server failed")),
Err(join_err) => {
Err(anyhow::anyhow!("supervisor metrics server task panicked: {join_err}"))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_zero_is_rejected() {
let mut cfg = SupervisorConfig::default();
let before = cfg.interval;
let result = apply_interval_override(&mut cfg, Some(0));
assert!(result.is_err(), "--interval 0 must be rejected");
assert_eq!(
cfg.interval, before,
"rejected interval must not mutate cfg"
);
}
#[test]
fn interval_positive_overrides() {
let mut cfg = SupervisorConfig::default();
apply_interval_override(&mut cfg, Some(45)).expect("positive interval is accepted");
assert_eq!(cfg.interval, std::time::Duration::from_secs(45));
}
#[test]
fn interval_none_keeps_config() {
let mut cfg = SupervisorConfig::default();
let before = cfg.interval;
apply_interval_override(&mut cfg, None).expect("absent interval is a no-op");
assert_eq!(cfg.interval, before);
}
}