diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index e734c224..55a65eae 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -159,10 +159,8 @@ pub async fn run_approval_apply_commit( approval_id: i64, ) -> Result<()> { let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::ApplyCommit)?; - // Create the bind-mount source dir (first-spawn may not have it yet). - // MCP listener registration is deferred to mcp_sockets::spawn_poll - // which fires within 10 s of the container coming up. - lifecycle::ensure_agent_runtime_dir(&approval.agent)?; + // Runtime dir creation is handled inside lifecycle::rebuild_no_meta's + // spawn path (first-spawn) or is already present for rebuilds. let agent_dir = Coordinator::agent_dir(&approval.agent); let applied_dir = Coordinator::agent_applied_dir(&approval.agent); coord.set_queue_step(queue_entry_id, "apply commit"); @@ -196,7 +194,6 @@ pub async fn run_approval_merge_config_pr( approval_id: i64, ) -> Result<()> { let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::MergeConfigPr)?; - lifecycle::ensure_agent_runtime_dir(&approval.agent)?; let agent_dir = Coordinator::agent_dir(&approval.agent); let applied_dir = Coordinator::agent_applied_dir(&approval.agent); coord.set_queue_step(queue_entry_id, "merge config pr"); diff --git a/hive-c0re/src/job_queue/exec.rs b/hive-c0re/src/job_queue/exec.rs index db94ca96..ff10eb71 100644 --- a/hive-c0re/src/job_queue/exec.rs +++ b/hive-c0re/src/job_queue/exec.rs @@ -181,11 +181,6 @@ async fn run_swap(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Res /// build+create — no prebuild needed). async fn run_create(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Result { let name = &claim.agent; - // 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); @@ -194,6 +189,9 @@ async fn run_create(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> R // (sync_agents commit) before `nixos-container create` — hold the // deploy-window gate so that commit can't land inside another // node's staged deploy window. + // Runtime dir creation and MCP listener registration are deferred to + // the tail Reconcile's converge_start_preamble / mcp_sockets supervisor + // so this node stays purely "provision + create", not "create + start". let _window = crate::meta::exclusive().await; crate::lifecycle::create_container(name, &hive, &paths).await?; Ok(NodeOutput::default()) diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index 8e58ac19..207976b0 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -275,6 +275,9 @@ async fn port_collision(self_name: &str) -> Option { pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> { create_container(name, hive, paths).await?; + // Runtime dir must exist before nixos-container start (nspawn bind-mount + // source). Create it here so callers don't need a separate preamble step. + ensure_agent_runtime_dir(name)?; write_dropins(name, hive, paths).await?; priv_run("start", name).await } @@ -629,6 +632,8 @@ pub async fn rebuild_no_meta( // See `docs/coordinator.md::Spawn path`. on_step("nixos-container create"); priv_run("create", name).await?; + // Runtime dir must exist before nixos-container start. + ensure_agent_runtime_dir(name)?; write_dropins(name, hive, paths).await?; on_step("nixos-container start"); priv_run("start", name).await?; diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 5bb12604..466fa410 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -202,14 +202,12 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { /// registration and notifying the manager on failure. async fn handle_spawn(coord: &Arc, name: &str) -> Result { tracing::info!(%name, "spawn"); - // Create the bind-mount source dir (pure filesystem, no Coordinator dep). - // MCP listener registration happens eagerly here (not deferred to the - // supervisor) so the socket is ready before the harness's first turn. - 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); + // lifecycle::spawn creates the runtime dir internally before start, so + // no manual ensure_agent_runtime_dir here. MCP listener registration is + // handled by mcp_sockets::spawn_poll on its first post-start tick. match lifecycle::spawn(name, &hive, &paths).await { Ok(()) => { if let Err(e) = coord.power.set(name, crate::power::Wanted::Up) { @@ -225,7 +223,6 @@ async fn handle_spawn(coord: &Arc, name: &str) -> Result { - // Roll back socket registration if container creation failed. coord.unregister_agent(name); coord.notify_manager(&hive_sh4re::HelperEvent::Spawned { agent: name.to_owned(), diff --git a/hive-c0re/src/workers/auto_update.rs b/hive-c0re/src/workers/auto_update.rs index b3c6fceb..1c7a909c 100644 --- a/hive-c0re/src/workers/auto_update.rs +++ b/hive-c0re/src/workers/auto_update.rs @@ -153,9 +153,8 @@ pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { return Ok(()); } tracing::info!("manager container missing — spawning"); - lifecycle::ensure_agent_runtime_dir(MANAGER_NAME)?; - // Manager has no MCP listener (socket_server::start_manager owns its - // socket); just need the dir + path value. + // lifecycle::spawn creates the runtime dir internally; no manual + // ensure_agent_runtime_dir needed here. let runtime = Coordinator::agent_dir(MANAGER_NAME); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(MANAGER_NAME, runtime);