use std::time::Duration;
use super::status::{AgentStatus, status_dir};
pub const STALE_AFTER: Duration = Duration::from_secs(7 * 24 * 60 * 60);
pub fn reconcile_orphaned_agents() -> Vec<AgentStatus> {
let interrupted = mark_orphans_interrupted();
match super::status::cleanup_stale(STALE_AFTER) {
Ok((scanned, removed)) if removed > 0 => {
tracing::info!(
target: "subagent",
"Swept {removed} stale sub-agent status file(s) of {scanned} scanned"
);
}
Ok(_) => {}
Err(e) => {
tracing::warn!(target: "subagent", "Sub-agent status sweep failed: {e}");
}
}
interrupted
}
fn mark_orphans_interrupted() -> Vec<AgentStatus> {
let dir = status_dir();
if !dir.exists() {
return Vec::new();
}
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) => {
tracing::warn!(
target: "subagent",
"Could not read sub-agent status dir {}: {e}",
dir.display()
);
return Vec::new();
}
};
let mut orphans: Vec<AgentStatus> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let data = match std::fs::read_to_string(&path) {
Ok(data) => data,
Err(e) => {
tracing::warn!(
target: "subagent",
"Could not read sub-agent status file {}: {e}",
path.display()
);
continue;
}
};
let mut status: AgentStatus = match serde_json::from_str(&data) {
Ok(status) => status,
Err(e) => {
tracing::warn!(
target: "subagent",
"Could not parse sub-agent status file {}: {e}",
path.display()
);
continue;
}
};
if status.state.is_terminal() {
continue;
}
if let Err(e) = status.mark_interrupted() {
tracing::error!(
target: "subagent",
"Sub-agent {} was interrupted by a restart but its status could not be updated, \
so it will keep reading as running: {e}",
status.id
);
}
tracing::warn!(
target: "subagent",
"Sub-agent '{}' ({}) for session {} was interrupted by a restart",
status.label,
status.id,
status.parent_session_id
);
orphans.push(status);
}
orphans.sort_by(|a, b| a.started_at.cmp(&b.started_at));
orphans
}