From 7426654a742f696e0fd179c7d98de8b7cbc94d0f Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 12:26:49 +0200 Subject: [PATCH] crash_watch: track prev_sub_agents to fix needs_login for newly spawned agents --- hive-c0re/src/actions.rs | 29 ++++++----------------------- hive-c0re/src/crash_watch.rs | 28 ++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 2a79cc6..877543f 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -210,24 +210,12 @@ fn finish_approval( }); } } - ApprovalKind::Spawn => { - coord.notify_manager(&HelperEvent::Spawned { - agent: approval.agent.clone(), - ok, - note, - sha: approval.fetched_sha.clone(), - }); - // Newly spawned container has no claude session yet. Emit - // NeedsLogin immediately so the manager prompts the operator - // rather than waiting for crash_watch (which would miss it - // because the agent appears simultaneously in both prev_needs - // and current_needs, making the diff empty on the first tick). - if ok { - coord.notify_manager(&HelperEvent::NeedsLogin { - agent: approval.agent.clone(), - }); - } - } + ApprovalKind::Spawn => coord.notify_manager(&HelperEvent::Spawned { + agent: approval.agent.clone(), + ok, + note, + sha: approval.fetched_sha.clone(), + }), ApprovalKind::ApplyCommit if is_first_spawn => { coord.notify_manager(&HelperEvent::Spawned { agent: approval.agent.clone(), @@ -235,11 +223,6 @@ fn finish_approval( note, sha: approval.fetched_sha.clone(), }); - if ok { - coord.notify_manager(&HelperEvent::NeedsLogin { - agent: approval.agent.clone(), - }); - } } ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt { agent: approval.agent.clone(), diff --git a/hive-c0re/src/crash_watch.rs b/hive-c0re/src/crash_watch.rs index a23083e..99bd3b0 100644 --- a/hive-c0re/src/crash_watch.rs +++ b/hive-c0re/src/crash_watch.rs @@ -30,6 +30,7 @@ pub fn spawn(coord: Arc) { tokio::spawn(async move { let mut prev_running: HashSet = HashSet::new(); let mut prev_logged_in: HashSet = HashSet::new(); + let mut prev_sub_agents: HashSet = HashSet::new(); let mut seeded = false; loop { let raw = lifecycle::list().await.unwrap_or_default(); @@ -59,7 +60,13 @@ pub fn spawn(coord: Arc) { if seeded { emit_crash_transitions(&coord, &prev_running, ¤t_running); - emit_login_transitions(&coord, &prev_logged_in, ¤t_logged_in, &sub_agents); + emit_login_transitions( + &coord, + &prev_logged_in, + ¤t_logged_in, + &sub_agents, + &prev_sub_agents, + ); } // Periodic container rescan — catches state flips that // happen outside our mutation surface (operator runs @@ -69,6 +76,7 @@ pub fn spawn(coord: Arc) { coord.rescan_containers_and_emit().await; prev_running = current_running; prev_logged_in = current_logged_in; + prev_sub_agents = sub_agents.into_iter().collect(); seeded = true; tokio::select! { @@ -110,6 +118,7 @@ fn emit_login_transitions( prev: &HashSet, current: &HashSet, sub_agents: &[String], + prev_sub_agents: &HashSet, ) { for agent in current.difference(prev) { tracing::info!(%agent, "agent logged in"); @@ -117,13 +126,16 @@ fn emit_login_transitions( agent: agent.clone(), }); } - // Only count NeedsLogin transitions for agents that exist and - // are *not* logged in — the difference set above already gives - // us "was in prev, gone from current" but we also want to fire - // for agents that newly appeared as not-logged-in (post-spawn / - // post-purge). Treat sub_agents minus current as the - // currently-needs-login set; emit when an agent enters it. - let prev_needs: HashSet<&str> = sub_agents + // Detect transitions into "needs login": an agent that was previously + // logged-in goes unsigned (credentials deleted), OR a brand-new agent + // appears without a session. + // + // prev_needs uses prev_sub_agents (the agent set from the last tick) so + // that a newly-spawned agent — which does not appear in prev_sub_agents — + // is absent from prev_needs even though it's not in prev_logged_in. + // Without this, new agents land in both prev_needs and current_needs and + // the set difference is empty, silently dropping the event. + let prev_needs: HashSet<&str> = prev_sub_agents .iter() .map(String::as_str) .filter(|n| !prev.contains(*n))