crash_watch: track prev_sub_agents to fix needs_login for newly spawned agents

This commit is contained in:
damocles 2026-05-22 12:26:49 +02:00 committed by Mara
parent 55fe2856b9
commit 7426654a74
2 changed files with 26 additions and 31 deletions

View file

@ -210,24 +210,12 @@ fn finish_approval(
}); });
} }
} }
ApprovalKind::Spawn => { ApprovalKind::Spawn => coord.notify_manager(&HelperEvent::Spawned {
coord.notify_manager(&HelperEvent::Spawned {
agent: approval.agent.clone(), agent: approval.agent.clone(),
ok, ok,
note, note,
sha: approval.fetched_sha.clone(), 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::ApplyCommit if is_first_spawn => { ApprovalKind::ApplyCommit if is_first_spawn => {
coord.notify_manager(&HelperEvent::Spawned { coord.notify_manager(&HelperEvent::Spawned {
agent: approval.agent.clone(), agent: approval.agent.clone(),
@ -235,11 +223,6 @@ fn finish_approval(
note, note,
sha: approval.fetched_sha.clone(), sha: approval.fetched_sha.clone(),
}); });
if ok {
coord.notify_manager(&HelperEvent::NeedsLogin {
agent: approval.agent.clone(),
});
}
} }
ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt { ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt {
agent: approval.agent.clone(), agent: approval.agent.clone(),

View file

@ -30,6 +30,7 @@ pub fn spawn(coord: Arc<Coordinator>) {
tokio::spawn(async move { tokio::spawn(async move {
let mut prev_running: HashSet<String> = HashSet::new(); let mut prev_running: HashSet<String> = HashSet::new();
let mut prev_logged_in: HashSet<String> = HashSet::new(); let mut prev_logged_in: HashSet<String> = HashSet::new();
let mut prev_sub_agents: HashSet<String> = HashSet::new();
let mut seeded = false; let mut seeded = false;
loop { loop {
let raw = lifecycle::list().await.unwrap_or_default(); let raw = lifecycle::list().await.unwrap_or_default();
@ -59,7 +60,13 @@ pub fn spawn(coord: Arc<Coordinator>) {
if seeded { if seeded {
emit_crash_transitions(&coord, &prev_running, &current_running); emit_crash_transitions(&coord, &prev_running, &current_running);
emit_login_transitions(&coord, &prev_logged_in, &current_logged_in, &sub_agents); emit_login_transitions(
&coord,
&prev_logged_in,
&current_logged_in,
&sub_agents,
&prev_sub_agents,
);
} }
// Periodic container rescan — catches state flips that // Periodic container rescan — catches state flips that
// happen outside our mutation surface (operator runs // happen outside our mutation surface (operator runs
@ -69,6 +76,7 @@ pub fn spawn(coord: Arc<Coordinator>) {
coord.rescan_containers_and_emit().await; coord.rescan_containers_and_emit().await;
prev_running = current_running; prev_running = current_running;
prev_logged_in = current_logged_in; prev_logged_in = current_logged_in;
prev_sub_agents = sub_agents.into_iter().collect();
seeded = true; seeded = true;
tokio::select! { tokio::select! {
@ -110,6 +118,7 @@ fn emit_login_transitions(
prev: &HashSet<String>, prev: &HashSet<String>,
current: &HashSet<String>, current: &HashSet<String>,
sub_agents: &[String], sub_agents: &[String],
prev_sub_agents: &HashSet<String>,
) { ) {
for agent in current.difference(prev) { for agent in current.difference(prev) {
tracing::info!(%agent, "agent logged in"); tracing::info!(%agent, "agent logged in");
@ -117,13 +126,16 @@ fn emit_login_transitions(
agent: agent.clone(), agent: agent.clone(),
}); });
} }
// Only count NeedsLogin transitions for agents that exist and // Detect transitions into "needs login": an agent that was previously
// are *not* logged in — the difference set above already gives // logged-in goes unsigned (credentials deleted), OR a brand-new agent
// us "was in prev, gone from current" but we also want to fire // appears without a session.
// for agents that newly appeared as not-logged-in (post-spawn / //
// post-purge). Treat sub_agents minus current as the // prev_needs uses prev_sub_agents (the agent set from the last tick) so
// currently-needs-login set; emit when an agent enters it. // that a newly-spawned agent — which does not appear in prev_sub_agents —
let prev_needs: HashSet<&str> = 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() .iter()
.map(String::as_str) .map(String::as_str)
.filter(|n| !prev.contains(*n)) .filter(|n| !prev.contains(*n))