use std::sync::Arc;
use tracing::{debug, error, info, warn};
use crate::activity::monitor::{ActivityMonitor, LlmClassifier};
use crate::session_manager::manager::ManagedError;
use crate::session_manager::{ManagedSessionState, SessionManager, SessionRecord};
use super::config::SupervisorConfig;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TickReport {
pub observed: usize,
pub resumed: Vec<String>,
pub resume_failures: usize,
pub classified: usize,
}
pub async fn run_tick<C: LlmClassifier>(
mgr: &Arc<SessionManager>,
cfg: &SupervisorConfig,
monitor: Option<&ActivityMonitor<C>>,
) -> TickReport {
crate::runtime::launch_spec::reap_orphans();
let records = mgr.list().await;
let mut report = TickReport {
observed: records.len(),
..Default::default()
};
for record in records {
match record.state {
ManagedSessionState::Stopped if cfg.auto_resume && record.is_auto_resumable() => {
let resume = mgr.resume_auto(&record.id);
let Some(outcome) = step_bounded(cfg, "resume_auto", &record, resume).await else {
report.resume_failures += 1;
continue;
};
match outcome {
Ok(_) => {
info!(
id = %record.id,
name = %record.tmux_name,
"supervisor: auto-resumed stopped session"
);
report.resumed.push(record.id.to_string());
}
Err(ManagedError::ResumeInFlight(_)) => {
info!(
id = %record.id,
name = %record.tmux_name,
"supervisor: another path is resuming this session; skipping this tick"
);
}
Err(ManagedError::AutoResumeRecorded(msg)) => {
error!(
id = %record.id,
name = %record.tmux_name,
"supervisor: auto-resume failed (already recorded on the record): {msg}"
);
report.resume_failures += 1;
}
Err(e) => settle_failed_resume(mgr, &record, e, &mut report).await,
}
}
ManagedSessionState::Active if cfg.classify_idle => {
if let Some(monitor) = monitor {
let classify = classify_active(mgr, monitor, &record.id, &record.tmux_name);
if step_bounded(cfg, "classify", &record, classify).await == Some(true) {
report.classified += 1;
}
}
}
_ => {}
}
}
debug!(
observed = report.observed,
resumed = report.resumed.len(),
resume_failures = report.resume_failures,
classified = report.classified,
"supervisor: sweep complete"
);
report
}
async fn step_bounded<F: std::future::Future>(
cfg: &SupervisorConfig,
step: &str,
record: &SessionRecord,
fut: F,
) -> Option<F::Output> {
let out = tokio::time::timeout(cfg.step_timeout, fut).await.ok();
if out.is_none() {
error!(
id = %record.id,
name = %record.tmux_name,
step,
limit_secs = cfg.step_timeout.as_secs(),
"supervisor: sweep step did not finish within its bound; abandoned it and \
moved to the next session (#8335)"
);
}
out
}
pub(super) async fn settle_failed_resume(
mgr: &Arc<SessionManager>,
record: &SessionRecord,
e: ManagedError,
report: &mut TickReport,
) {
if matches!(e, ManagedError::InvalidState(..))
&& mgr
.get(&record.id)
.await
.is_ok_and(|now| now.state == ManagedSessionState::Active)
{
info!(
id = %record.id,
name = %record.tmux_name,
"supervisor: session is already active; nothing to resume"
);
return;
}
error!(
id = %record.id,
name = %record.tmux_name,
"supervisor: auto-resume failed: {e}"
);
if let Err(mark_err) = mgr
.mark_errored(&record.id, &format!("auto-resume failed: {e}"))
.await
{
error!(
id = %record.id,
name = %record.tmux_name,
"supervisor: could not mark failed auto-resume as errored: {mark_err}"
);
}
report.resume_failures += 1;
}
async fn classify_active<C: LlmClassifier>(
mgr: &Arc<SessionManager>,
monitor: &ActivityMonitor<C>,
id: &crate::session_manager::ManagedSessionId,
tmux_name: &str,
) -> bool {
let pane = match mgr.capture_pane(id, 60).await {
Ok(text) => text,
Err(e) => {
warn!(id = %id, name = %tmux_name, "supervisor: pane capture failed: {e}");
return false;
}
};
match monitor.check(&id.to_string(), &pane).await {
Ok(result) => {
debug!(
id = %id,
name = %tmux_name,
state = ?result.verdict.state,
cache_hit = result.cache_hit,
"supervisor: classified idle session"
);
true
}
Err(e) => {
warn!(id = %id, name = %tmux_name, "supervisor: classification failed: {e}");
false
}
}
}