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:
Dark-Alex-17 committed 2026-09-16 19:55:52 -06:00
1 parent 739c1be8bb
commit a3b71a6889
7 files changed
+369 -4

No files matched your search

+50
View File
@@ -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);