From 613d58e70108418209cff7d4062f9b4781ebfb12 Mon Sep 17 00:00:00 2001 From: Harry <4210852+harryert@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:40:23 +0800 Subject: [PATCH] fix(ecc2): exclude template and started assignment notices from rerouting --- ecc2/src/session/manager.rs | 5 ++++- ecc2/src/session/store.rs | 21 ++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 02ee33999..ce5806ce8 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -1091,7 +1091,10 @@ pub async fn rebalance_team_backlog( } let message_budget = limit.saturating_sub(outcomes.len()); - let messages = db.unread_task_handoffs_for_rebalance(&delegate.id, message_budget)?; + // Only unassigned intake work may move. Native assignment notices + // already belong to the running/queued delegate and must never be + // replayed onto another worker by the load balancer. + let messages = db.unread_task_handoffs_for_session(&delegate.id, message_budget)?; for message in messages { if outcomes.len() >= limit { diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index 62f9a1159..b3aa9be67 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -4974,6 +4974,11 @@ fn is_assignment_handoff(content: &str, sender: &str) -> bool { return false; } + // Template steps are already queued with their own runner; their handoff + // is lineage/notification, never a second intake request. + if context.contains(" | template ") { + return true; + } [ "reused idle delegate", "spawned new delegate", @@ -7058,9 +7063,15 @@ mod tests { priority: comms::TaskPriority::Normal, })?; db.send_message("lead", "worker", &native_delegate, "task_handoff")?; - assert_eq!(db.unread_task_handoff_count("worker")?, 2); + let template_notice = serde_json::to_string(&comms::MessageType::TaskHandoff { + task: "Run template step".to_string(), + context: "Assigned by lead [claude] | cwd /repo | template release | build".to_string(), + priority: comms::TaskPriority::Normal, + })?; + db.send_message("lead", "worker", &template_notice, "task_handoff")?; + assert_eq!(db.unread_task_handoff_count("worker")?, 3); assert!(db.unread_task_handoff_targets(10)?.is_empty()); - assert_eq!(db.unread_task_handoffs_for_rebalance("worker", 10)?.len(), 2); + assert_eq!(db.unread_task_handoffs_for_rebalance("worker", 10)?.len(), 3); assert!(db .unread_task_handoffs_for_session("worker", 10)? .is_empty()); @@ -7079,7 +7090,7 @@ mod tests { priority: comms::TaskPriority::Normal, })?; db.send_message(long_sender, "worker", &long_notice, "task_handoff")?; - assert_eq!(db.unread_task_handoff_count("worker")?, 3); + assert_eq!(db.unread_task_handoff_count("worker")?, 4); assert!(db.unread_task_handoff_targets(10)?.is_empty()); // Even a matching routing phrase in a user message is not an internal @@ -7092,14 +7103,14 @@ mod tests { r#"{"task":"Explain spawned new delegate","context":"Explicit task"}"#, "task_handoff", )?; - assert_eq!(db.unread_task_handoff_count("worker")?, 6); + assert_eq!(db.unread_task_handoff_count("worker")?, 7); assert_eq!(db.unread_task_handoff_targets(10)?, vec![("worker".to_string(), 3)]); assert_eq!(db.unread_task_handoffs_for_session("worker", 10)?.len(), 3); // The original notification remains available to the human/agent inbox, // even though it never spawns a recursive grandchild. let all = db.list_messages_for_session("worker", 10)?; - assert_eq!(all.iter().filter(|message| message.msg_type == "task_handoff").count(), 6); + assert_eq!(all.iter().filter(|message| message.msg_type == "task_handoff").count(), 7); Ok(()) }