//! MCP socket listener reconcile loop. //! //! Periodically checks that every running agent container has a bound //! MCP listener registered in the `Coordinator`. Any agent that is running //! but whose listener has gone (e.g. after a hive-c0re restart that cleared //! `/run/hyperhive/agents/`) gets re-registered automatically. //! //! Same shape as `agent_sockets::spawn_poll` — a simple 10 s tick loop that //! converges "agent container running ⇒ MCP listener bound". The self-healing //! guarantee means no callsite needs to call `register_agent` directly; //! `lifecycle::ensure_agent_runtime_dir` (called inside `lifecycle::spawn` //! and `converge_start_preamble`) creates the bind-mount source, and the //! reconcile loop picks up the listener binding on the next tick. use std::sync::Arc; use crate::coordinator::Coordinator; /// Spawn the MCP socket listener reconcile loop. /// /// Every 10 s the loop lists running agent containers and calls /// `register_agent` for any that lack a bound listener. The first tick fires /// immediately so hive-c0re restarts re-register all running agents without /// waiting a full interval. pub fn spawn_poll(coord: Arc) { tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(10)); loop { interval.tick().await; reconcile_once(&coord).await; } }); } async fn reconcile_once(coord: &Arc) { let running = match crate::lifecycle::list().await { Ok(names) => names, Err(e) => { tracing::debug!(error = ?e, "mcp_sockets poll: failed to list agents"); return; } }; let registered = coord.list_agents(); for container in running { let Some(name) = container.strip_prefix(crate::lifecycle::AGENT_PREFIX) else { continue; }; if !registered.contains(&name.to_owned()) { if let Err(e) = coord.register_agent(name) { tracing::warn!( agent = %name, error = ?e, "mcp_sockets poll: register_agent failed" ); } else { tracing::debug!(agent = %name, "mcp_sockets poll: registered missing listener"); } } } }