use std::time::Duration;
use crate::brain::agent::service::work_status::{self, WorkKind, WorkStatus, status_dir};
pub const STALE_AFTER: Duration = Duration::from_secs(7 * 24 * 60 * 60);
pub fn reconcile_orphaned_agents() -> Vec<WorkStatus> {
let migrated = work_status::migrate_legacy_dir(&work_status::legacy_dir());
if migrated > 0 {
tracing::info!(
target: "subagent",
"Migrated {migrated} legacy sub-agent status file(s) into the unified dir"
);
}
let interrupted = mark_orphans_interrupted();
match work_status::cleanup_stale(STALE_AFTER) {
Ok((scanned, removed)) if removed > 0 => {
tracing::info!(
target: "subagent",
"Swept {removed} stale detached-work status file(s) of {scanned} scanned"
);
}
Ok(_) => {}
Err(e) => {
tracing::warn!(target: "subagent", "Detached-work status sweep failed: {e}");
}
}
interrupted
.into_iter()
.filter(|status| status.kind == WorkKind::Agent)
.collect()
}
fn mark_orphans_interrupted() -> Vec<WorkStatus> {
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 detached-work status dir {}: {e}",
dir.display()
);
return Vec::new();
}
};
let mut orphans: Vec<WorkStatus> = 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 detached-work status file {}: {e}",
path.display()
);
continue;
}
};
let mut status: WorkStatus = match serde_json::from_str(&data) {
Ok(status) => status,
Err(e) => {
tracing::warn!(
target: "subagent",
"Could not parse detached-work status file {}: {e}",
path.display()
);
continue;
}
};
if status.state.is_terminal() {
continue;
}
if let Err(e) = status.mark_interrupted() {
tracing::error!(
target: "subagent",
"Detached work {} was interrupted by a restart but its status could not be \
updated, so it will keep reading as running: {e}",
status.id
);
}
match status.kind {
WorkKind::Agent => tracing::warn!(
target: "subagent",
"Sub-agent '{}' ({}) for session {} was interrupted by a restart",
status.label,
status.id,
status.session_id
),
WorkKind::Command => tracing::warn!(
target: "background_task",
"Detached command '{}' ({}) for session {} was interrupted by a restart \
(report rides the DB row, #763)",
status.label,
status.id,
status.session_id
),
}
orphans.push(status);
}
orphans.sort_by(|a, b| a.spawned_at.cmp(&b.spawned_at));
orphans
}