From 2fba71fcdbb46414a0fbae59a62994c7fde8ff2b Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 8 Apr 2026 03:55:03 -0700 Subject: [PATCH] feat: align ecc2 delegate backlog semantics --- ecc2/src/session/manager.rs | 85 +++++++++++++++++++++++++++++++------ ecc2/src/tui/dashboard.rs | 31 ++++++-------- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index a2c1a741..ee210c41 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -42,7 +42,10 @@ pub fn get_status(db: &StateStore, id: &str) -> Result { pub fn get_team_status(db: &StateStore, id: &str, depth: usize) -> Result { let root = resolve_session(db, id)?; - let unread_counts = db.unread_message_counts()?; + let handoff_backlog = db + .unread_task_handoff_targets(db.list_sessions()?.len().max(1))? + .into_iter() + .collect(); let mut visited = HashSet::new(); visited.insert(root.id.clone()); @@ -52,14 +55,14 @@ pub fn get_team_status(db: &StateStore, id: &str, depth: usize) -> Result, + handoff_backlog: &std::collections::HashMap, visited: &mut HashSet, descendants: &mut Vec, ) -> Result<()> { @@ -571,7 +574,7 @@ fn collect_delegation_descendants( descendants.push(DelegatedSessionSummary { depth: current_depth, - unread_messages: unread_counts.get(&child_id).copied().unwrap_or(0), + handoff_backlog: handoff_backlog.get(&child_id).copied().unwrap_or(0), session, }); @@ -580,7 +583,7 @@ fn collect_delegation_descendants( &child_id, remaining_depth.saturating_sub(1), current_depth + 1, - unread_counts, + handoff_backlog, visited, descendants, )?; @@ -843,14 +846,13 @@ fn summarize_backlog_pressure( agent_type: &str, targets: &[(String, usize)], ) -> Result { - let unread_counts = db.unread_message_counts()?; let mut summary = BacklogPressureSummary::default(); for (session_id, _) in targets { let delegates = direct_delegate_sessions(db, session_id, agent_type)?; let has_clear_idle_delegate = delegates.iter().any(|delegate| { delegate.state == SessionState::Idle - && unread_counts.get(&delegate.id).copied().unwrap_or(0) == 0 + && db.unread_task_handoff_count(&delegate.id).unwrap_or(0) == 0 }); let has_capacity = delegates.len() < cfg.max_parallel_sessions; @@ -1048,7 +1050,7 @@ pub struct SessionStatus { pub struct TeamStatus { root: Session, - unread_messages: std::collections::HashMap, + handoff_backlog: std::collections::HashMap, descendants: Vec, } @@ -1112,7 +1114,7 @@ struct BacklogPressureSummary { struct DelegatedSessionSummary { depth: usize, - unread_messages: usize, + handoff_backlog: usize, session: Session, } @@ -1154,8 +1156,8 @@ impl fmt::Display for TeamStatus { writeln!(f, "Branch: {}", worktree.branch)?; } - let lead_unread = self.unread_messages.get(&self.root.id).copied().unwrap_or(0); - writeln!(f, "Inbox: {}", lead_unread)?; + let lead_handoff_backlog = self.handoff_backlog.get(&self.root.id).copied().unwrap_or(0); + writeln!(f, "Backlog: {}", lead_handoff_backlog)?; if self.descendants.is_empty() { return write!(f, "Board: no delegated sessions"); @@ -1185,11 +1187,11 @@ impl fmt::Display for TeamStatus { for item in items { writeln!( f, - " - {}{} [{}] | inbox {} | {}", + " - {}{} [{}] | backlog {} handoff(s) | {}", " ".repeat(item.depth.saturating_sub(1)), item.session.id, item.session.agent_type, - item.unread_messages, + item.handoff_backlog, item.session.task )?; } @@ -2404,4 +2406,59 @@ mod tests { Ok(()) } + + #[test] + fn team_status_reports_handoff_backlog_not_generic_inbox_noise() -> Result<()> { + let tempdir = TestDir::new("manager-team-status-backlog")?; + let repo_root = tempdir.path().join("repo"); + init_git_repo(&repo_root)?; + + let cfg = build_config(tempdir.path()); + let db = StateStore::open(&cfg.db_path)?; + let now = Utc::now(); + + db.insert_session(&Session { + id: "lead".to_string(), + task: "lead task".to_string(), + agent_type: "claude".to_string(), + working_dir: repo_root.clone(), + state: SessionState::Running, + pid: Some(42), + worktree: None, + created_at: now - Duration::minutes(4), + updated_at: now - Duration::minutes(4), + metrics: SessionMetrics::default(), + })?; + db.insert_session(&Session { + id: "worker".to_string(), + task: "delegate task".to_string(), + agent_type: "claude".to_string(), + working_dir: repo_root, + state: SessionState::Idle, + pid: None, + worktree: None, + created_at: now - Duration::minutes(3), + updated_at: now - Duration::minutes(3), + metrics: SessionMetrics::default(), + })?; + + db.send_message("lead", "worker", "FYI status update", "info")?; + db.send_message( + "lead", + "worker", + "{\"task\":\"Delegated work\",\"context\":\"Delegated from lead\"}", + "task_handoff", + )?; + let _ = db.mark_messages_read("worker")?; + db.send_message("lead", "worker", "FYI reminder", "info")?; + + let status = get_team_status(&db, "lead", 3)?; + let rendered = format!("{status}"); + + assert!(rendered.contains("Backlog: 0")); + assert!(rendered.contains("| backlog 0 handoff(s) |")); + assert!(!rendered.contains("Inbox:")); + + Ok(()) + } } diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index 11133ba6..dfdba905 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -104,7 +104,7 @@ struct AggregateUsage { struct DelegatedChildSummary { session_id: String, state: SessionState, - unread_messages: usize, + handoff_backlog: usize, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -1297,11 +1297,6 @@ impl Dashboard { match self.db.get_session(&child_id) { Ok(Some(session)) => { team.total += 1; - let unread_messages = self - .unread_message_counts - .get(&child_id) - .copied() - .unwrap_or(0); let handoff_backlog = match self.db.unread_task_handoff_count(&child_id) { Ok(count) => count, Err(error) => { @@ -1323,12 +1318,12 @@ impl Dashboard { } route_candidates.push(DelegatedChildSummary { - unread_messages: handoff_backlog, + handoff_backlog, state: state.clone(), session_id: child_id.clone(), }); delegated.push(DelegatedChildSummary { - unread_messages, + handoff_backlog, state, session_id: child_id, }); @@ -1365,7 +1360,7 @@ impl Dashboard { ) -> Option { if let Some(idle_clear) = delegates .iter() - .filter(|delegate| delegate.state == SessionState::Idle && delegate.unread_messages == 0) + .filter(|delegate| delegate.state == SessionState::Idle && delegate.handoff_backlog == 0) .min_by_key(|delegate| delegate.session_id.as_str()) { return Some(format!( @@ -1381,24 +1376,24 @@ impl Dashboard { if let Some(idle_backed_up) = delegates .iter() .filter(|delegate| delegate.state == SessionState::Idle) - .min_by_key(|delegate| (delegate.unread_messages, delegate.session_id.as_str())) + .min_by_key(|delegate| (delegate.handoff_backlog, delegate.session_id.as_str())) { return Some(format!( - "reuse idle {} with inbox {}", + "reuse idle {} with backlog {}", format_session_id(&idle_backed_up.session_id), - idle_backed_up.unread_messages + idle_backed_up.handoff_backlog )); } if let Some(active_delegate) = delegates .iter() .filter(|delegate| matches!(delegate.state, SessionState::Running | SessionState::Pending)) - .min_by_key(|delegate| (delegate.unread_messages, delegate.session_id.as_str())) + .min_by_key(|delegate| (delegate.handoff_backlog, delegate.session_id.as_str())) { return Some(format!( - "reuse active {} with inbox {}", + "reuse active {} with backlog {}", format_session_id(&active_delegate.session_id), - active_delegate.unread_messages + active_delegate.handoff_backlog )); } @@ -1588,10 +1583,10 @@ impl Dashboard { lines.push("Delegates".to_string()); for child in &self.selected_child_sessions { lines.push(format!( - "- {} [{}] | inbox {}", + "- {} [{}] | backlog {}", format_session_id(&child.session_id), session_state_label(&child.state), - child.unread_messages + child.handoff_backlog )); } } @@ -2422,7 +2417,7 @@ mod tests { Some("reuse idle idle-wor") ); assert_eq!(dashboard.selected_child_sessions.len(), 1); - assert_eq!(dashboard.selected_child_sessions[0].unread_messages, 1); + assert_eq!(dashboard.selected_child_sessions[0].handoff_backlog, 0); } #[test]