Both accept loops returned on the first accept() error. The AgentSocket stayed in Coordinator.agents, so mcp_sockets::sync_on_start skipped the agent, and only a Start node or a hive-c0re restart bound it again. The unit sets no LimitNOFILE, so a single EMFILE at the 1024-fd soft limit cut the agent off from send/recv/ack with one warn line as the only trace. Both loops now share accept_until_fatal. Connection errors (ECONNABORTED/ECONNRESET/ECONNREFUSED) retry at once, any other error retries after 1 s, following axum's serve loop that the dashboard already runs. Errors that leave the listening fd unusable (EBADF, EFAULT, EINVAL, ENOTSOCK, EOPNOTSUPP) log at error and exit the process: nothing re-binds a listener while hive-c0re runs, the manager listener has no re-bind path at all, and the unit's Restart=on-failure restart runs start_manager and sync_on_start, which re-bind every socket. mcp_sockets.rs now states that invariant instead of asserting that a listener can only disappear on restart. LimitNOFILE is left unset: per-agent fd use is a listener plus a Recv long-poll plus short-lived requests, and a higher limit would raise the memory ceiling of the per-line bound (bound x open connections). Closes #4721
54 lines
2.2 KiB
Rust
54 lines
2.2 KiB
Rust
//! MCP socket listener boot sync.
|
|
//!
|
|
//! On hive-c0re startup, any agent containers that survived the daemon restart
|
|
//! still have their bind-mount source dirs but no live MCP listener (the
|
|
//! `Coordinator` is freshly empty). `sync_on_start` does a one-shot sweep to
|
|
//! re-register all running agents.
|
|
//!
|
|
//! After startup, listeners are managed event-driven:
|
|
//! - `run_create` calls `register_agent` eagerly on first-spawn.
|
|
//! - `run_reconcile` calls `register_agent` immediately after `start_with_fallback`.
|
|
//! - `kill`/`destroy` paths call `unregister_agent`.
|
|
//!
|
|
//! No recurring poll is needed because c0re owns the listener lifecycle. An
|
|
//! accept loop retries every error except one that leaves its fd unusable,
|
|
//! and on that one hive-c0re exits (`socket_server::exit_on_dead_listener`),
|
|
//! so a listener only disappears across a restart — exactly the case
|
|
//! `sync_on_start` covers.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::coordinator::Coordinator;
|
|
|
|
/// One-shot MCP listener sync run at daemon startup.
|
|
///
|
|
/// Iterates all currently-running agent containers and calls `register_agent`
|
|
/// for any that have no live listener in the `Coordinator`. Safe to call
|
|
/// concurrently with the rest of startup — `register_agent` is idempotent
|
|
/// (drops and rebinds) and the coordinator lock serialises concurrent calls.
|
|
pub async fn sync_on_start(coord: Arc<Coordinator>) {
|
|
let running = match crate::lifecycle::list().await {
|
|
Ok(names) => names,
|
|
Err(e) => {
|
|
tracing::warn!(error = ?e, "mcp_sockets: startup sync failed to list agents; MCP listeners may be missing until next start");
|
|
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: startup register_agent failed"
|
|
);
|
|
} else {
|
|
tracing::debug!(agent = %name, "mcp_sockets: registered listener on startup");
|
|
}
|
|
}
|
|
}
|
|
}
|