feat(TASK-049): classify idle-prompt ctrl-c + ctrl-d as a completed exit
Ctrl-C at the reedline prompt no longer latches the abort signal unconditionally: latch_prompt_interrupt only marks an interruption when the supervisor had live agents/jobs to cancel (new Supervisor::has_active_tasks), so the advertised exit path (ctrl-c at an idle prompt, then ctrl-d) fires agent.completed at teardown instead of agent.interrupted. Mid-turn ctrl-c still latches directly via wait_user_interrupt/poll_abort_signal, preserving the interrupted-then-exit classification. Also: probe-lane tests for escalation.answered single-fire, escalation question env truncation, agent.failed inverse gate, and hooks probe Scenario I (session.started vs session.resumed); comment notes on escalation.answered semantics and the agent__cancel blind spot.
This commit is contained in:
7 files changed
+369
-4
No files matched your search
@@ -190,6 +190,12 @@ impl Supervisor {
|
||||
.count()
|
||||
}
|
||||
|
||||
/// True when any registered agent or job is still running -- i.e. a
|
||||
/// cancellation right now would actually interrupt in-flight work.
|
||||
pub fn has_active_tasks(&self) -> bool {
|
||||
self.effective_active_count() > 0 || self.active_job_count() > 0
|
||||
}
|
||||
|
||||
pub fn max_concurrent(&self) -> usize {
|
||||
self.max_concurrent
|
||||
}
|
||||
@@ -449,6 +455,50 @@ mod tests {
|
||||
assert!(result.unwrap_err().to_string().contains("at capacity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_active_tasks_false_when_empty_or_all_finished() {
|
||||
let mut sup = Supervisor::new(4, 3);
|
||||
assert!(!sup.has_active_tasks());
|
||||
// make_handle drops its runtime, aborting the task, so the
|
||||
// registered agent is already finished.
|
||||
sup.register(make_handle("a1", "explore", 1)).unwrap();
|
||||
assert!(!sup.has_active_tasks());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_active_tasks_true_with_running_agent() {
|
||||
// Keep the runtime alive so the task is never polled and the agent
|
||||
// counts as running.
|
||||
let rt = Builder::new_current_thread().enable_all().build().unwrap();
|
||||
let join_handle = rt.spawn(async {
|
||||
Ok::<AgentResult, Error>(AgentResult {
|
||||
id: "done".into(),
|
||||
agent_name: "test".into(),
|
||||
output: "result".into(),
|
||||
exit_status: AgentExitStatus::Completed,
|
||||
})
|
||||
});
|
||||
let running_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,
|
||||
};
|
||||
let mut sup = Supervisor::new(4, 3);
|
||||
sup.register(running_handle).unwrap();
|
||||
assert!(sup.has_active_tasks());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_active_tasks_true_with_running_job() {
|
||||
let mut sup = Supervisor::new(4, 3).with_max_concurrent_jobs(1);
|
||||
sup.register(make_job("j1", create_abort_signal())).unwrap();
|
||||
assert!(sup.has_active_tasks());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_register_rejects_exceeding_depth() {
|
||||
let mut sup = Supervisor::new(4, 2);
|
||||
|
||||
Reference in new issue
Block a user