feat(jobs): push background-job completion notifications via per-context queue
- add NotificationQueue/SystemNotification: every context owns a fresh queue (children never inherit the parent's, avoiding first-drainer-wins races between transcripts) - job tasks push job_completed/job_failed events on completion, failure, and timeout; a panic skips the push and is surfaced by the guardrail's finished-handle enumeration and collect's JoinError mapping instead - events for jobs already collected or cancelled are dropped at drain time by filtering against live supervisor registration - replace inject_escalation_notification with single-pass merge_system_channel: pending_escalations (root-only) ordered before system_notifications (any depth) on the last tool result of a batch; byte-identical output when notifications are empty, proven by the unmodified pre-merger characterization tests
This commit is contained in:
@@ -34,6 +34,7 @@ use crate::rag::Rag;
|
||||
use crate::supervisor::Supervisor;
|
||||
use crate::supervisor::escalation::EscalationQueue;
|
||||
use crate::supervisor::mailbox::Inbox;
|
||||
use crate::supervisor::notification::NotificationQueue;
|
||||
use crate::utils::{
|
||||
AbortSignal, abortable_run_with_spinner, edit_file, fuzzy_filter, get_env_name,
|
||||
list_file_names, now, render_prompt, temp_file,
|
||||
@@ -330,6 +331,7 @@ pub struct RequestContext {
|
||||
pub self_agent_id: Option<String>,
|
||||
pub inbox: Option<Arc<Inbox>>,
|
||||
pub escalation_queue: Option<Arc<EscalationQueue>>,
|
||||
pub notification_queue: Arc<NotificationQueue>,
|
||||
pub current_depth: usize,
|
||||
pub auto_continue_count: usize,
|
||||
pub pending_agents_guardrail_count: u32,
|
||||
@@ -364,6 +366,7 @@ impl RequestContext {
|
||||
self_agent_id: None,
|
||||
inbox: None,
|
||||
escalation_queue: None,
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth: 0,
|
||||
auto_continue_count: 0,
|
||||
pending_agents_guardrail_count: 0,
|
||||
@@ -424,6 +427,7 @@ impl RequestContext {
|
||||
self_agent_id: None,
|
||||
inbox: None,
|
||||
escalation_queue: None,
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth: 0,
|
||||
auto_continue_count: 0,
|
||||
pending_agents_guardrail_count: 0,
|
||||
@@ -471,6 +475,7 @@ impl RequestContext {
|
||||
self_agent_id: self.self_agent_id.clone(),
|
||||
inbox: self.inbox.clone(),
|
||||
escalation_queue: self.escalation_queue.clone(),
|
||||
notification_queue: self.notification_queue.clone(),
|
||||
current_depth: self.current_depth,
|
||||
auto_continue_count: 0,
|
||||
pending_agents_guardrail_count: 0,
|
||||
@@ -516,6 +521,7 @@ impl RequestContext {
|
||||
self_agent_id: Some(self_agent_id),
|
||||
inbox: Some(inbox),
|
||||
escalation_queue: parent.escalation_queue.clone(),
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth,
|
||||
auto_continue_count: 0,
|
||||
pending_agents_guardrail_count: 0,
|
||||
@@ -4202,6 +4208,7 @@ impl RequestContext {
|
||||
self.supervisor = supervisor;
|
||||
self.inbox = None;
|
||||
self.escalation_queue = None;
|
||||
self.notification_queue = Arc::new(NotificationQueue::new());
|
||||
self.self_agent_id = None;
|
||||
self.parent_supervisor = None;
|
||||
self.current_depth = 0;
|
||||
@@ -4244,6 +4251,7 @@ impl RequestContext {
|
||||
self.self_agent_id = None;
|
||||
self.inbox = None;
|
||||
self.escalation_queue = None;
|
||||
self.notification_queue = Arc::new(NotificationQueue::new());
|
||||
self.current_depth = 0;
|
||||
self.auto_continue_count = 0;
|
||||
self.pending_agents_guardrail_count = 0;
|
||||
@@ -5463,6 +5471,32 @@ mod tests {
|
||||
assert!(ctx.root_escalation_queue().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_for_child_gets_fresh_notification_queue() {
|
||||
let parent = create_test_ctx();
|
||||
let child = RequestContext::new_for_child(
|
||||
Arc::clone(&parent.app),
|
||||
&parent,
|
||||
1,
|
||||
Arc::new(Inbox::new()),
|
||||
"agent_test_1".to_string(),
|
||||
);
|
||||
assert!(
|
||||
!Arc::ptr_eq(&parent.notification_queue, &child.notification_queue),
|
||||
"each child owns its notifications; a shared queue would race drains"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_for_branch_shares_notification_queue() {
|
||||
let ctx = create_test_ctx();
|
||||
let branch = ctx.fork_for_branch();
|
||||
assert!(Arc::ptr_eq(
|
||||
&ctx.notification_queue,
|
||||
&branch.notification_queue
|
||||
));
|
||||
}
|
||||
|
||||
fn app_state_with_mcp_config(mcp_server_support: bool, server_names: &[&str]) -> Arc<AppState> {
|
||||
app_state_with_mcp_command(mcp_server_support, server_names, "echo")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user