Skip to content

Commit 2fba71f

Browse files
committed
feat: align ecc2 delegate backlog semantics
1 parent 63c437b commit 2fba71f

2 files changed

Lines changed: 84 additions & 32 deletions

File tree

ecc2/src/session/manager.rs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
4242

4343
pub fn get_team_status(db: &StateStore, id: &str, depth: usize) -> Result<TeamStatus> {
4444
let root = resolve_session(db, id)?;
45-
let unread_counts = db.unread_message_counts()?;
45+
let handoff_backlog = db
46+
.unread_task_handoff_targets(db.list_sessions()?.len().max(1))?
47+
.into_iter()
48+
.collect();
4649
let mut visited = HashSet::new();
4750
visited.insert(root.id.clone());
4851

@@ -52,14 +55,14 @@ pub fn get_team_status(db: &StateStore, id: &str, depth: usize) -> Result<TeamSt
5255
&root.id,
5356
depth,
5457
1,
55-
&unread_counts,
58+
&handoff_backlog,
5659
&mut visited,
5760
&mut descendants,
5861
)?;
5962

6063
Ok(TeamStatus {
6164
root,
62-
unread_messages: unread_counts,
65+
handoff_backlog,
6366
descendants,
6467
})
6568
}
@@ -552,7 +555,7 @@ fn collect_delegation_descendants(
552555
session_id: &str,
553556
remaining_depth: usize,
554557
current_depth: usize,
555-
unread_counts: &std::collections::HashMap<String, usize>,
558+
handoff_backlog: &std::collections::HashMap<String, usize>,
556559
visited: &mut HashSet<String>,
557560
descendants: &mut Vec<DelegatedSessionSummary>,
558561
) -> Result<()> {
@@ -571,7 +574,7 @@ fn collect_delegation_descendants(
571574

572575
descendants.push(DelegatedSessionSummary {
573576
depth: current_depth,
574-
unread_messages: unread_counts.get(&child_id).copied().unwrap_or(0),
577+
handoff_backlog: handoff_backlog.get(&child_id).copied().unwrap_or(0),
575578
session,
576579
});
577580

@@ -580,7 +583,7 @@ fn collect_delegation_descendants(
580583
&child_id,
581584
remaining_depth.saturating_sub(1),
582585
current_depth + 1,
583-
unread_counts,
586+
handoff_backlog,
584587
visited,
585588
descendants,
586589
)?;
@@ -843,14 +846,13 @@ fn summarize_backlog_pressure(
843846
agent_type: &str,
844847
targets: &[(String, usize)],
845848
) -> Result<BacklogPressureSummary> {
846-
let unread_counts = db.unread_message_counts()?;
847849
let mut summary = BacklogPressureSummary::default();
848850

849851
for (session_id, _) in targets {
850852
let delegates = direct_delegate_sessions(db, session_id, agent_type)?;
851853
let has_clear_idle_delegate = delegates.iter().any(|delegate| {
852854
delegate.state == SessionState::Idle
853-
&& unread_counts.get(&delegate.id).copied().unwrap_or(0) == 0
855+
&& db.unread_task_handoff_count(&delegate.id).unwrap_or(0) == 0
854856
});
855857
let has_capacity = delegates.len() < cfg.max_parallel_sessions;
856858

@@ -1048,7 +1050,7 @@ pub struct SessionStatus {
10481050

10491051
pub struct TeamStatus {
10501052
root: Session,
1051-
unread_messages: std::collections::HashMap<String, usize>,
1053+
handoff_backlog: std::collections::HashMap<String, usize>,
10521054
descendants: Vec<DelegatedSessionSummary>,
10531055
}
10541056

@@ -1112,7 +1114,7 @@ struct BacklogPressureSummary {
11121114

11131115
struct DelegatedSessionSummary {
11141116
depth: usize,
1115-
unread_messages: usize,
1117+
handoff_backlog: usize,
11161118
session: Session,
11171119
}
11181120

@@ -1154,8 +1156,8 @@ impl fmt::Display for TeamStatus {
11541156
writeln!(f, "Branch: {}", worktree.branch)?;
11551157
}
11561158

1157-
let lead_unread = self.unread_messages.get(&self.root.id).copied().unwrap_or(0);
1158-
writeln!(f, "Inbox: {}", lead_unread)?;
1159+
let lead_handoff_backlog = self.handoff_backlog.get(&self.root.id).copied().unwrap_or(0);
1160+
writeln!(f, "Backlog: {}", lead_handoff_backlog)?;
11591161

11601162
if self.descendants.is_empty() {
11611163
return write!(f, "Board: no delegated sessions");
@@ -1185,11 +1187,11 @@ impl fmt::Display for TeamStatus {
11851187
for item in items {
11861188
writeln!(
11871189
f,
1188-
" - {}{} [{}] | inbox {} | {}",
1190+
" - {}{} [{}] | backlog {} handoff(s) | {}",
11891191
" ".repeat(item.depth.saturating_sub(1)),
11901192
item.session.id,
11911193
item.session.agent_type,
1192-
item.unread_messages,
1194+
item.handoff_backlog,
11931195
item.session.task
11941196
)?;
11951197
}
@@ -2404,4 +2406,59 @@ mod tests {
24042406

24052407
Ok(())
24062408
}
2409+
2410+
#[test]
2411+
fn team_status_reports_handoff_backlog_not_generic_inbox_noise() -> Result<()> {
2412+
let tempdir = TestDir::new("manager-team-status-backlog")?;
2413+
let repo_root = tempdir.path().join("repo");
2414+
init_git_repo(&repo_root)?;
2415+
2416+
let cfg = build_config(tempdir.path());
2417+
let db = StateStore::open(&cfg.db_path)?;
2418+
let now = Utc::now();
2419+
2420+
db.insert_session(&Session {
2421+
id: "lead".to_string(),
2422+
task: "lead task".to_string(),
2423+
agent_type: "claude".to_string(),
2424+
working_dir: repo_root.clone(),
2425+
state: SessionState::Running,
2426+
pid: Some(42),
2427+
worktree: None,
2428+
created_at: now - Duration::minutes(4),
2429+
updated_at: now - Duration::minutes(4),
2430+
metrics: SessionMetrics::default(),
2431+
})?;
2432+
db.insert_session(&Session {
2433+
id: "worker".to_string(),
2434+
task: "delegate task".to_string(),
2435+
agent_type: "claude".to_string(),
2436+
working_dir: repo_root,
2437+
state: SessionState::Idle,
2438+
pid: None,
2439+
worktree: None,
2440+
created_at: now - Duration::minutes(3),
2441+
updated_at: now - Duration::minutes(3),
2442+
metrics: SessionMetrics::default(),
2443+
})?;
2444+
2445+
db.send_message("lead", "worker", "FYI status update", "info")?;
2446+
db.send_message(
2447+
"lead",
2448+
"worker",
2449+
"{\"task\":\"Delegated work\",\"context\":\"Delegated from lead\"}",
2450+
"task_handoff",
2451+
)?;
2452+
let _ = db.mark_messages_read("worker")?;
2453+
db.send_message("lead", "worker", "FYI reminder", "info")?;
2454+
2455+
let status = get_team_status(&db, "lead", 3)?;
2456+
let rendered = format!("{status}");
2457+
2458+
assert!(rendered.contains("Backlog: 0"));
2459+
assert!(rendered.contains("| backlog 0 handoff(s) |"));
2460+
assert!(!rendered.contains("Inbox:"));
2461+
2462+
Ok(())
2463+
}
24072464
}

ecc2/src/tui/dashboard.rs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ struct AggregateUsage {
104104
struct DelegatedChildSummary {
105105
session_id: String,
106106
state: SessionState,
107-
unread_messages: usize,
107+
handoff_backlog: usize,
108108
}
109109

110110
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -1297,11 +1297,6 @@ impl Dashboard {
12971297
match self.db.get_session(&child_id) {
12981298
Ok(Some(session)) => {
12991299
team.total += 1;
1300-
let unread_messages = self
1301-
.unread_message_counts
1302-
.get(&child_id)
1303-
.copied()
1304-
.unwrap_or(0);
13051300
let handoff_backlog = match self.db.unread_task_handoff_count(&child_id) {
13061301
Ok(count) => count,
13071302
Err(error) => {
@@ -1323,12 +1318,12 @@ impl Dashboard {
13231318
}
13241319

13251320
route_candidates.push(DelegatedChildSummary {
1326-
unread_messages: handoff_backlog,
1321+
handoff_backlog,
13271322
state: state.clone(),
13281323
session_id: child_id.clone(),
13291324
});
13301325
delegated.push(DelegatedChildSummary {
1331-
unread_messages,
1326+
handoff_backlog,
13321327
state,
13331328
session_id: child_id,
13341329
});
@@ -1365,7 +1360,7 @@ impl Dashboard {
13651360
) -> Option<String> {
13661361
if let Some(idle_clear) = delegates
13671362
.iter()
1368-
.filter(|delegate| delegate.state == SessionState::Idle && delegate.unread_messages == 0)
1363+
.filter(|delegate| delegate.state == SessionState::Idle && delegate.handoff_backlog == 0)
13691364
.min_by_key(|delegate| delegate.session_id.as_str())
13701365
{
13711366
return Some(format!(
@@ -1381,24 +1376,24 @@ impl Dashboard {
13811376
if let Some(idle_backed_up) = delegates
13821377
.iter()
13831378
.filter(|delegate| delegate.state == SessionState::Idle)
1384-
.min_by_key(|delegate| (delegate.unread_messages, delegate.session_id.as_str()))
1379+
.min_by_key(|delegate| (delegate.handoff_backlog, delegate.session_id.as_str()))
13851380
{
13861381
return Some(format!(
1387-
"reuse idle {} with inbox {}",
1382+
"reuse idle {} with backlog {}",
13881383
format_session_id(&idle_backed_up.session_id),
1389-
idle_backed_up.unread_messages
1384+
idle_backed_up.handoff_backlog
13901385
));
13911386
}
13921387

13931388
if let Some(active_delegate) = delegates
13941389
.iter()
13951390
.filter(|delegate| matches!(delegate.state, SessionState::Running | SessionState::Pending))
1396-
.min_by_key(|delegate| (delegate.unread_messages, delegate.session_id.as_str()))
1391+
.min_by_key(|delegate| (delegate.handoff_backlog, delegate.session_id.as_str()))
13971392
{
13981393
return Some(format!(
1399-
"reuse active {} with inbox {}",
1394+
"reuse active {} with backlog {}",
14001395
format_session_id(&active_delegate.session_id),
1401-
active_delegate.unread_messages
1396+
active_delegate.handoff_backlog
14021397
));
14031398
}
14041399

@@ -1588,10 +1583,10 @@ impl Dashboard {
15881583
lines.push("Delegates".to_string());
15891584
for child in &self.selected_child_sessions {
15901585
lines.push(format!(
1591-
"- {} [{}] | inbox {}",
1586+
"- {} [{}] | backlog {}",
15921587
format_session_id(&child.session_id),
15931588
session_state_label(&child.state),
1594-
child.unread_messages
1589+
child.handoff_backlog
15951590
));
15961591
}
15971592
}
@@ -2422,7 +2417,7 @@ mod tests {
24222417
Some("reuse idle idle-wor")
24232418
);
24242419
assert_eq!(dashboard.selected_child_sessions.len(), 1);
2425-
assert_eq!(dashboard.selected_child_sessions[0].unread_messages, 1);
2420+
assert_eq!(dashboard.selected_child_sessions[0].handoff_backlog, 0);
24262421
}
24272422

24282423
#[test]

0 commit comments

Comments
 (0)