fix: let ctrl-c interrupt blocking job and agent collects

This commit is contained in:
Dark-Alex-17 committed 2026-09-03 13:03:10 -06:00
1 parent dc41e9d5df
commit 3b01ae8f18
6 files changed
+223 -3

No files matched your search

+88 -2
View File
@@ -7,7 +7,7 @@ use crate::config::{
use crate::supervisor::mailbox::{Envelope, EnvelopePayload, Inbox};
use crate::supervisor::notification::agent_notification;
use crate::supervisor::{AgentExitStatus, AgentHandle, AgentResult, Supervisor, TaskKind};
use crate::utils::{AbortSignal, create_abort_signal, wait_abort_signal};
use crate::utils::{AbortSignal, create_abort_signal, wait_abort_signal, wait_user_interrupt};
use crate::graph;
use anyhow::{Context, Result, anyhow, bail};
@@ -1031,6 +1031,14 @@ async fn handle_collect(ctx: &mut RequestContext, args: &Value) -> Result<Value>
sup.abort_signal_for(id)
};
let interrupted = || {
json!({
"status": "interrupted",
"id": id,
"message": format!("agent__collect was interrupted by the user; agent '{id}' is still running in the background. Collect it again later, cancel it with agent__cancel, or continue with other work."),
})
};
loop {
let is_finished = {
let sup = supervisor.read();
@@ -1041,6 +1049,10 @@ async fn handle_collect(ctx: &mut RequestContext, args: &Value) -> Result<Value>
break;
}
if ctx.session_abort.as_ref().is_some_and(|s| s.aborted()) {
return Ok(interrupted());
}
if let Some(queue) = ctx.root_escalation_queue()
&& queue.has_pending()
{
@@ -1068,10 +1080,18 @@ async fn handle_collect(ctx: &mut RequestContext, args: &Value) -> Result<Value>
tokio::select! {
_ = time::sleep(Duration::from_millis(200)) => {}
_ = wait_abort_signal(abort) => {}
_ = wait_user_interrupt(ctx.session_abort.as_ref()) => {
return Ok(interrupted());
}
}
}
None => {
time::sleep(Duration::from_millis(200)).await;
tokio::select! {
_ = time::sleep(Duration::from_millis(200)) => {}
_ = wait_user_interrupt(ctx.session_abort.as_ref()) => {
return Ok(interrupted());
}
}
}
}
}
@@ -2557,6 +2577,72 @@ mod tests {
);
}
#[test]
fn handle_collect_session_interrupt_returns_and_keeps_agent() {
run_async(async {
let mut ctx = ctx_with_supervisor(4, 3);
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let join_handle = tokio::spawn(async move {
let _ = rx.await;
Ok(AgentResult {
id: "a1".to_string(),
agent_name: "explore".to_string(),
output: "late output".to_string(),
exit_status: AgentExitStatus::Completed,
})
});
let handle = AgentHandle {
id: "a1".to_string(),
agent_name: "explore".to_string(),
depth: 1,
inbox: Arc::new(Inbox::new()),
abort_signal: create_abort_signal(),
join_handle,
child_supervisor: None,
};
ctx.supervisor
.as_ref()
.unwrap()
.write()
.register(handle)
.unwrap();
let session = create_abort_signal();
session.set_ctrlc();
ctx.session_abort = Some(session.clone());
let result = time::timeout(
Duration::from_secs(2),
handle_collect(&mut ctx, &json!({"id": "a1"})),
)
.await
.expect("interrupted collect must return promptly")
.unwrap();
assert_eq!(result["status"], "interrupted");
assert_eq!(result["id"], "a1");
assert!(
result["message"]
.as_str()
.unwrap()
.contains("still running")
);
assert_eq!(
ctx.supervisor.as_ref().unwrap().read().is_finished("a1"),
Some(false)
);
session.reset();
tx.send(()).unwrap();
let collected = handle_collect(&mut ctx, &json!({"id": "a1"}))
.await
.unwrap();
assert_eq!(collected["status"], "completed");
assert_eq!(collected["output"], "late output");
});
}
#[test]
fn handle_collect_pending_escalations_early_out_keeps_handle() {
let rt = tokio::runtime::Builder::new_current_thread()