feat(#2290): split ensure_runtime — dirs to lifecycle, listeners to mcp_sockets supervisor

- lifecycle::ensure_agent_runtime_dir(name): pure filesystem op, no
  Coordinator dep. Creates /run/hyperhive/agents/<name> without touching
  the MCP listener map.

- workers/mcp_sockets::spawn_poll(coord): 10 s reconcile loop (same shape
  as agent_sockets::spawn_poll). Converges 'agent running => MCP listener
  bound'. First tick is immediate so hive-c0re restarts re-register all
  running agents without waiting a full interval. Fixes the dead-listener-
  after-daemon-restart gap.

- All ensure_runtime() call sites updated:
  - Prebuild/Swap/WriteDropin: Coordinator::agent_dir() (pure, no IO)
  - Reconcile-Start: ensure_agent_runtime_dir + agent_dir (dir may be
    missing after reboot; listener deferred to supervisor)
  - run_create / handle_spawn: ensure_agent_runtime_dir + register_agent
    (eager on first spawn so socket ready before harness first turn)
  - apply_commit / merge_config_pr: ensure_agent_runtime_dir + agent_dir
  - Manager (auto_update): ensure_agent_runtime_dir + agent_dir
    (manager has no MCP listener; socket_server::start_manager owns it)

- ensure_runtime() retained in Coordinator with updated doc pointing at
  the preferred split form. No callers remain outside tests.
This commit is contained in:
atlas 2026-07-08 23:27:48 +02:00
commit 3d919b596f
10 changed files with 147 additions and 30 deletions

View file

@ -97,9 +97,10 @@ async fn run_prebuild(
relock: bool,
) -> Result<NodeOutput> {
let name = &claim.agent;
let agent_dir = coord
.ensure_runtime(name)
.with_context(|| format!("ensure_runtime {name}"))?;
// Prebuild runs while the agent is still up — the runtime dir and
// MCP listener already exist. Use the pure path accessor; no need
// to re-register the listener (the mcp_sockets supervisor owns that).
let agent_dir = Coordinator::agent_dir(name);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
crate::lifecycle::prepare_rebuild_dirs(name, &paths).await?;
@ -131,7 +132,9 @@ async fn run_prebuild(
/// `Reconcile` runs after this node terminal ok *or* fail.
async fn run_swap(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Result<NodeOutput> {
let name = &claim.agent;
let agent_dir = coord.ensure_runtime(name)?;
// Swap runs on an already-existing (stopped) container — runtime dir
// and listener were created earlier. Pure path accessor suffices.
let agent_dir = Coordinator::agent_dir(name);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
let result =
@ -178,7 +181,12 @@ async fn run_swap(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Res
/// build+create — no prebuild needed).
async fn run_create(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Result<NodeOutput> {
let name = &claim.agent;
let agent_dir = coord.ensure_runtime(name)?;
// First-spawn: create the bind-mount source dir (tmpfs — empty after
// reboot). Register the MCP listener eagerly so it's ready when the
// tail Reconcile starts the container and the harness connects.
crate::lifecycle::ensure_agent_runtime_dir(name)?;
coord.register_agent(name)?;
let agent_dir = Coordinator::agent_dir(name);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
ctx.step("nixos-container create");
@ -254,7 +262,13 @@ async fn run_reconcile(
// Prebuild/Swap nodes; the bare-Reconcile templates (boot
// reconcile, plain start/restart) otherwise start with
// nothing under /run and fail.
let agent_dir = coord.ensure_runtime(name)?;
//
// Dir creation is the pure-filesystem part (no Coordinator
// dep). The MCP listener is reconciled by mcp_sockets::spawn_poll
// whose first tick fires immediately on daemon start — the
// container boot takes longer than the 10 s interval anyway.
crate::lifecycle::ensure_agent_runtime_dir(name)?;
let agent_dir = Coordinator::agent_dir(name);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
crate::lifecycle::write_dropins(name, &hive, &paths).await?;
@ -336,7 +350,10 @@ async fn run_drain(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Re
/// `set_nspawn_flags` + `set_resource_limits` + daemon-reload.
async fn run_write_dropin(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOutput> {
let name = &claim.agent;
let agent_dir = coord.ensure_runtime(name)?;
// write_dropins only needs the path value to build AgentPaths; the
// dir doesn't need to exist at this point (created by ensure_runtime
// on the upstream Prebuild/Start node).
let agent_dir = Coordinator::agent_dir(name);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(name, agent_dir);
crate::lifecycle::write_dropins(name, &hive, &paths).await?;