pub mod api;
pub mod audit;
pub mod bug_report;
pub mod claude_config;
pub mod coordinator;
pub mod discover;
pub mod discovery;
pub mod doctor;
pub mod error;
pub mod idle_nudge;
pub mod idle_reaper;
pub mod llm_overseer;
pub mod lock;
pub mod managed_routes;
pub mod manager;
pub mod mcp_backend;
pub mod mcp_bugreport;
pub mod mcp_console;
pub mod mcp_context;
pub mod mcp_project;
pub mod mcp_proxy;
pub mod mcp_session;
pub mod openapi;
pub mod optimizer;
pub mod orphan_gc;
pub mod overseer_compose;
pub mod pairing_store;
pub mod provisioning;
pub mod runtime_reap;
pub mod services;
pub(crate) mod session_record_kind;
pub mod sm_stdio;
pub(crate) mod spawn_command;
pub mod state;
pub mod tmux;
pub mod watcher;
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::info;
pub use state::DaemonState;
pub async fn run_http(state: Arc<DaemonState>, addr: SocketAddr) -> anyhow::Result<()> {
info!("trusty-mpm daemon starting on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await?;
serve_http(state, listener).await
}
pub async fn serve_http(
state: Arc<DaemonState>,
listener: tokio::net::TcpListener,
) -> anyhow::Result<()> {
if tmux::TmuxDriver::is_available() {
info!("tmux control model available");
} else {
info!("tmux not found — sessions will need the PTY or SDK control model");
}
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
let addrs = discover::discover_all(&home).await;
info!(
"trusty-memory at {}, trusty-search at {}",
addrs.memory, addrs.search
);
state.set_trusty_addrs(addrs);
let discovered = discovery::discover_all(&state).await;
if discovered.adopted > 0 {
info!(
"auto-discovered {} Claude Code session(s)",
discovered.adopted
);
}
let cancel = tokio_util::sync::CancellationToken::new();
let fw = watcher::FileWatcher::new(Arc::clone(&state));
tokio::spawn(fw.spawn(cancel.child_token()));
tokio::spawn(reap_loop(Arc::clone(&state), cancel.child_token()));
spawn_idle_reaper_if_enabled(Arc::clone(&state), cancel.child_token()).await;
if orphan_gc_enabled() {
tokio::spawn(orphan_gc_loop(Arc::clone(&state), cancel.child_token()));
} else {
info!("orphan-GC disabled via TRUSTY_MPM_ORPHAN_GC");
}
if inproject_hygiene_enabled() {
let repos_root = managed_routes::inproject::repos_root();
tokio::task::spawn_blocking(move || {
managed_routes::inproject_hygiene::run_hygiene_for_all_bases(&repos_root);
});
} else {
info!("inproject-hygiene disabled via TRUSTY_MPM_INPROJECT_HYGIENE");
}
let app = api::origin_guard::guard_router(
api::router(Arc::clone(&state)),
trusty_common::server::SelfOrigins::default(),
);
info!(
"daemon listening (trusty-mpm v{} pid {}); press Ctrl-C to stop",
env!("CARGO_PKG_VERSION"),
std::process::id()
);
let reaper_state = Arc::clone(&state);
axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(async move {
shutdown_signal().await;
cancel.cancel();
let mgr = reaper_state.session_manager().await;
mgr.shutdown().await;
reap_all_live_sessions(reaper_state).await;
})
.await?;
Ok(())
}
async fn shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let mut term = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
tracing::warn!("failed to register SIGTERM handler: {e}");
let _ = tokio::signal::ctrl_c().await;
return;
}
};
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
}
async fn reap_all_live_sessions(state: Arc<DaemonState>) {
let Ok(driver) = tmux::TmuxDriver::discover() else {
info!("graceful shutdown: tmux unavailable; no sessions to reap");
return;
};
let names: std::collections::HashSet<String> = state
.list_sessions()
.into_iter()
.map(|s| s.tmux_name)
.collect();
let mut reaped = 0usize;
for name in names {
match driver.kill_session(&name) {
Ok(()) => reaped += 1,
Err(e) => {
tracing::warn!(
"graceful shutdown: kill_session({name}) failed (may already be gone): {e}"
);
}
}
}
info!("graceful shutdown: reaped {reaped} legacy session(s)");
}
async fn spawn_idle_reaper_if_enabled(
state: Arc<DaemonState>,
cancel: tokio_util::sync::CancellationToken,
) {
let cfg = crate::core::config::MpmConfig::load_default().idle_auto_stop;
if !cfg.enabled {
tracing::debug!("idle-reaper: disabled (idle_auto_stop.enabled = false); skipping");
return;
}
info!(
dry_run = cfg.dry_run,
poll_secs = cfg.poll_interval_secs,
idle_threshold = cfg.idle_consecutive_threshold,
done_threshold = cfg.done_consecutive_threshold,
"idle-reaper: enabled; spawning background loop{}",
if cfg.dry_run {
" (report-only — set idle_auto_stop.dry_run=false to enact teardown)"
} else {
""
}
);
let mgr = state.session_manager().await;
let provider = Arc::new(DaemonVerdictProvider {
mgr: Arc::clone(&mgr),
state,
});
tokio::spawn(idle_reaper::idle_reaper_loop(mgr, cfg, provider, cancel));
}
struct DaemonVerdictProvider {
mgr: Arc<crate::session_manager::SessionManager>,
state: Arc<DaemonState>,
}
impl idle_reaper::IdleVerdictProvider for DaemonVerdictProvider {
async fn verdict(&self, session_name: &str) -> Option<String> {
use crate::activity::ActivityState;
let pane_text = self
.mgr
.capture_pane_by_tmux_name(session_name, 300)
.await?;
let monitor = self.state.activity_monitor();
match monitor.check(session_name, &pane_text).await {
Ok(result) => {
let s = match result.verdict.state {
ActivityState::Idle => "idle",
ActivityState::Done => "done",
ActivityState::Working => "working",
ActivityState::BlockedOnPermission => "blocked-on-permission",
ActivityState::Errored => "errored",
ActivityState::Unknown => "unknown",
};
Some(s.to_string())
}
Err(e) => {
tracing::debug!(name = %session_name, "idle-reaper: activity check failed: {e}");
None
}
}
}
}
const REAP_INTERVAL_SECS: u64 = 60;
async fn reap_loop(state: Arc<DaemonState>, cancel: tokio_util::sync::CancellationToken) {
let mut tick = tokio::time::interval(std::time::Duration::from_secs(REAP_INTERVAL_SECS));
loop {
tokio::select! {
_ = cancel.cancelled() => {
info!("reap_loop: cancel signal received; exiting");
break;
}
_ = tick.tick() => {
if let Ok(driver) = tmux::TmuxDriver::discover() {
let result = state.reap_dead_sessions(&driver);
if result.reaped > 0 {
info!("reaped {} dead session(s)", result.reaped);
}
if result.stopped > 0 {
info!(
"marked {} session(s) stopped (claude process exited)",
result.stopped
);
}
state.reap_dead_managed_sessions(&driver).await;
let mgr = state.session_manager().await;
let stopped = runtime_reap::reap_runtime_exited_managed(
&mgr,
&driver,
&orphan_gc::ProcessTreeProbe,
)
.await;
if stopped > 0 {
info!(
"marked {stopped} managed session(s) stopped (runtime process exited, #1814)"
);
}
}
}
}
}
}
const ORPHAN_GC_INTERVAL_SECS: u64 = 60;
fn orphan_gc_enabled() -> bool {
parse_orphan_gc_enabled(std::env::var("TRUSTY_MPM_ORPHAN_GC").ok().as_deref())
}
fn parse_orphan_gc_enabled(raw: Option<&str>) -> bool {
match raw {
Some(v) => {
let v = v.trim().to_ascii_lowercase();
!matches!(v.as_str(), "0" | "false" | "off" | "no")
}
None => true,
}
}
fn inproject_hygiene_enabled() -> bool {
parse_inproject_hygiene_enabled(
std::env::var("TRUSTY_MPM_INPROJECT_HYGIENE")
.ok()
.as_deref(),
)
}
fn parse_inproject_hygiene_enabled(raw: Option<&str>) -> bool {
match raw {
Some(v) => {
let v = v.trim().to_ascii_lowercase();
!matches!(v.as_str(), "0" | "false" | "off" | "no")
}
None => true,
}
}
fn orphan_gc_interval_secs() -> u64 {
parse_orphan_gc_interval(
std::env::var("TRUSTY_MPM_ORPHAN_GC_INTERVAL_SECS")
.ok()
.as_deref(),
)
}
fn parse_orphan_gc_interval(raw: Option<&str>) -> u64 {
raw.and_then(|v| v.trim().parse::<u64>().ok())
.filter(|n| *n > 0)
.unwrap_or(ORPHAN_GC_INTERVAL_SECS)
}
async fn orphan_gc_loop(state: Arc<DaemonState>, cancel: tokio_util::sync::CancellationToken) {
let interval_secs = orphan_gc_interval_secs();
info!(
interval_secs,
"orphan-GC enabled; running startup sweep then periodic reconciliation"
);
let mut gc = orphan_gc::OrphanGc::new();
let mut pid_gc = crate::core::pid_registry::PidOrphanGc::new();
let probe = orphan_gc::ProcessTreeProbe;
let mut tick = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
loop {
tokio::select! {
_ = cancel.cancelled() => {
info!("orphan_gc_loop: cancel signal received; exiting");
break;
}
_ = tick.tick() => {
let Ok(driver) = tmux::TmuxDriver::discover() else {
continue;
};
let panes = match driver.list_managed_panes() {
Ok(p) => p,
Err(e) => {
tracing::warn!("orphan-GC: list_managed_panes failed: {e}");
continue;
}
};
let tracked = state.gather_tracked_names().await;
let mgr = state.session_manager().await;
let mtmux = mgr.tmux_driver();
let reaped = orphan_gc::run_sweep(&mut gc, &panes, &tracked, &probe, mtmux.as_ref());
if reaped > 0 {
info!("orphan-GC reaped {reaped} orphaned managed session(s)");
}
let live_ids = state.gather_live_session_ids().await;
let pid_outcome = state.pid_registry().sweep_orphans(
&live_ids,
&crate::core::pid_registry::OsProcessProbe,
&mut pid_gc,
);
if pid_outcome.terminated > 0
|| pid_outcome.removed_stale > 0
|| pid_outcome.deferred > 0
{
info!(
terminated = pid_outcome.terminated,
removed_stale = pid_outcome.removed_stale,
deferred = pid_outcome.deferred,
"orphan-GC PID-file sweep cleaned up orphaned claude process(es)"
);
}
let max_age = chrono::Duration::hours(crate::session_manager::MAX_EPHEMERAL_AGE_HOURS);
match mgr.reap_aged_ephemeral(max_age).await {
Ok(n) if n > 0 => info!("ephemeral auto-reap decommissioned {n} stale session(s)"),
Ok(_) => {}
Err(e) => tracing::warn!("ephemeral auto-reap failed: {e}"),
}
let repos_root = managed_routes::inproject::repos_root();
match mgr.reap_orphaned_worktrees(&repos_root).await {
Ok(outcome) => {
if !outcome.removed.is_empty() {
info!(
"orphan-GC reaped {} orphaned worktree dir(s)",
outcome.removed.len()
);
}
if !outcome.owner_unknown.is_empty() {
info!(
"orphan-GC left {} owner-unknown worktree dir(s) untouched \
(run `tm doctor` to review, #3649)",
outcome.owner_unknown.len()
);
}
}
Err(e) => tracing::warn!("orphan-GC worktree sweep failed: {e}"),
}
match mgr.reap_orphaned_search_indexes().await {
Ok(removed) if !removed.is_empty() => {
info!(
"orphan-GC removed {} orphaned trusty-search index(es)",
removed.len()
);
}
Ok(_) => {}
Err(e) => tracing::warn!("orphan-GC search-index sweep failed: {e}"),
}
}
}
}
}
pub async fn run_mcp(state: Arc<DaemonState>) -> anyhow::Result<()> {
info!("trusty-mpm MCP server starting on stdio");
let backend = mcp_backend::StateBackend::new(state);
trusty_common::mcp::run_stdio_loop(move |req| {
let backend = backend.clone();
async move { crate::mcp::dispatch(&backend, req).await }
})
.await
}
#[cfg(test)]
mod shutdown_reaper_tests {
use super::{DaemonState, reap_all_live_sessions, reap_loop};
use std::sync::Arc;
#[tokio::test]
async fn reap_all_live_sessions_is_safe_when_empty() {
let state: Arc<DaemonState> = DaemonState::shared();
reap_all_live_sessions(state).await;
}
#[tokio::test]
async fn cancel_token_stops_reap_loop_cleanly() {
let state: Arc<DaemonState> = DaemonState::shared();
let cancel = tokio_util::sync::CancellationToken::new();
let token = cancel.clone();
let handle = tokio::spawn(reap_loop(state, token));
cancel.cancel();
handle
.await
.expect("reap_loop task must not panic after cancellation");
}
}
#[cfg(test)]
mod orphan_gc_config_tests {
use super::{ORPHAN_GC_INTERVAL_SECS, parse_orphan_gc_enabled, parse_orphan_gc_interval};
#[test]
fn parse_orphan_gc_enabled_default_and_overrides() {
assert!(parse_orphan_gc_enabled(None), "unset must default ON");
for off in ["0", "false", "off", "no", "OFF", " false "] {
assert!(!parse_orphan_gc_enabled(Some(off)), "{off:?} must disable");
}
for on in ["1", "true", "yes", "anything", " "] {
assert!(
parse_orphan_gc_enabled(Some(on)),
"{on:?} must keep enabled"
);
}
}
#[test]
fn parse_orphan_gc_interval_override() {
assert_eq!(parse_orphan_gc_interval(None), ORPHAN_GC_INTERVAL_SECS);
assert_eq!(parse_orphan_gc_interval(Some("30")), 30);
assert_eq!(parse_orphan_gc_interval(Some(" 45 ")), 45);
for bad in ["0", "nonsense", "-5", ""] {
assert_eq!(
parse_orphan_gc_interval(Some(bad)),
ORPHAN_GC_INTERVAL_SECS,
"{bad:?}"
);
}
}
}
#[cfg(test)]
mod inproject_hygiene_config_tests {
use super::parse_inproject_hygiene_enabled;
#[test]
fn parse_inproject_hygiene_enabled_default_and_overrides() {
assert!(
parse_inproject_hygiene_enabled(None),
"unset must default ON"
);
for off in ["0", "false", "off", "no", "OFF", " false "] {
assert!(
!parse_inproject_hygiene_enabled(Some(off)),
"{off:?} must disable"
);
}
for on in ["1", "true", "yes", "anything", " "] {
assert!(
parse_inproject_hygiene_enabled(Some(on)),
"{on:?} must keep enabled"
);
}
}
}