use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use sqlx::sqlite::SqlitePool;
use tokio::time::interval;
use crate::acp::chat_persistence;
use crate::acp::client::TurnEndEvent;
use crate::acp::supervisor::AcpSupervisor;
pub const IDLE_RECYCLE_SECS: u64 = 300;
pub const REQUIRES_ACTION_RECYCLE_SECS: u64 = 1800;
pub const PERMISSION_TIMEOUT_NOTICE: &str = "权限请求 30 分钟未获响应,系统已自动取消该请求并回收会话(agent 已终止)。可重新打开会话继续。";
pub const PROMPT_STALE_SECS: u64 = 600;
const TICK_SECS: u64 = 30;
pub async fn run_reaper(
supervisor: AcpSupervisor,
db: SqlitePool,
idle_recycle_secs: Arc<AtomicU64>,
) {
let mut ticker = interval(Duration::from_secs(TICK_SECS));
loop {
ticker.tick().await;
let idle_secs = idle_recycle_secs.load(Ordering::Relaxed);
let mut to_reap: Vec<(String, bool /*perm_stale*/)> = Vec::new();
for (sid, client) in supervisor.snapshot().await {
if client.is_idle_stale(idle_secs).await {
to_reap.push((sid, false));
} else if client.is_permission_stale(REQUIRES_ACTION_RECYCLE_SECS).await {
to_reap.push((sid, true));
} else if client.is_prompt_stale(PROMPT_STALE_SECS) {
tracing::warn!(
session_id = %sid,
"prompt active but no agent activity for {}s; force-finalizing turn",
PROMPT_STALE_SECS
);
client.mark_prompt_idle();
client.notify_turn_end(TurnEndEvent::Done {
stop_reason: "InactivityTimeout".into(),
row_id: client.turn_row_id(),
duration: client.turn_timing(),
});
}
}
for (sid, perm_stale) in to_reap {
if let Some(client) = supervisor.dispose(&sid).await {
if perm_stale {
let blocks = serde_json::json!([
{ "type": "system", "label": PERMISSION_TIMEOUT_NOTICE }
])
.to_string();
if let Err(e) = chat_persistence::insert_message(
&db,
&sid,
"system",
PERMISSION_TIMEOUT_NOTICE,
Some(&blocks),
)
.await
{
tracing::warn!(
session_id = %sid,
error = %e,
"reaper: failed to persist permission-timeout system message"
);
}
client.notify_system_message(PERMISSION_TIMEOUT_NOTICE.to_string());
let _ = client.cancel();
}
client.shutdown().await;
}
}
}
}