From 48420890e052f4ce2bff9c5e96417b1792a17cc9 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 24 May 2026 12:08:41 +0200 Subject: [PATCH 1/4] lifecycle: preserve stopped state across rebuild (closes #371) --- hive-c0re/src/lifecycle.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index fd3986a6..472867f8 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -336,14 +336,25 @@ pub async fn rebuild_no_meta( let container = container_name(name); let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); if container_exists(name).await { - // Existing container: update nspawn flags, then rebuild + restart - // so any bind-mount / networking changes take effect. + // Existing container: preserve the prior running state across + // rebuild (closes #371). If it was running, cycle stop+start so + // any bind-mount / networking changes in the nspawn conf take + // effect. If it was stopped, leave it stopped — even if + // `nixos-container update` brought it up to run the in-container + // switch, the operator's explicit "stopped" intent wins. + let was_running = is_running(name).await; set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir)?; set_resource_limits(&container)?; systemd_daemon_reload().await?; run(&["update", &container, "--flake", &flake_ref]).await?; - run(&["stop", &container]).await?; - run(&["start", &container]).await + if was_running { + run(&["stop", &container]).await?; + run(&["start", &container]).await + } else if is_running(name).await { + run(&["stop", &container]).await + } else { + Ok(()) + } } else { // First spawn: create the container first (which writes the nspawn // conf file), then overwrite with our flags and start. From 0540f213f1624341469348ebedd5817471d16a8d Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 24 May 2026 13:03:31 +0200 Subject: [PATCH 2/4] lifecycle: use nixos-container restart for the running case (mara@#372) --- hive-c0re/src/lifecycle.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 472867f8..bff0965c 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -348,8 +348,7 @@ pub async fn rebuild_no_meta( systemd_daemon_reload().await?; run(&["update", &container, "--flake", &flake_ref]).await?; if was_running { - run(&["stop", &container]).await?; - run(&["start", &container]).await + run(&["restart", &container]).await } else if is_running(name).await { run(&["stop", &container]).await } else { From bc872fffb97b3b63e4fafe717141e54e29493356 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 24 May 2026 13:12:01 +0200 Subject: [PATCH 3/4] lifecycle: drop defensive is_running check per mara@#372 --- hive-c0re/src/lifecycle.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index bff0965c..2f6b622a 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -337,11 +337,17 @@ pub async fn rebuild_no_meta( let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); if container_exists(name).await { // Existing container: preserve the prior running state across - // rebuild (closes #371). If it was running, cycle stop+start so - // any bind-mount / networking changes in the nspawn conf take - // effect. If it was stopped, leave it stopped — even if - // `nixos-container update` brought it up to run the in-container - // switch, the operator's explicit "stopped" intent wins. + // rebuild (closes #371). `nixos-container update` itself is + // already state-preserving — its only side effect on the running + // unit is a `systemctl reload container@` gated on + // `isContainerRunning`, so a stopped container stays stopped + // after the build + profile bump. We only need to recycle the + // outer unit when the container was running, so that any + // changes to `/etc/nixos-containers/.conf` (EXTRA_NSPAWN_FLAGS) + // or our systemd drop-ins (resource limits, stale-mount + // cleanup) are re-read by the next nspawn invocation. If a + // racing operator action started the container during the + // rebuild, that's their intent — don't override. let was_running = is_running(name).await; set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir)?; set_resource_limits(&container)?; @@ -349,8 +355,6 @@ pub async fn rebuild_no_meta( run(&["update", &container, "--flake", &flake_ref]).await?; if was_running { run(&["restart", &container]).await - } else if is_running(name).await { - run(&["stop", &container]).await } else { Ok(()) } From 14b79f43cfa787fda9ea1f3830de5f7c547487e1 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 24 May 2026 13:15:26 +0200 Subject: [PATCH 4/4] lifecycle: stop before update for boot-style apply (mara@#372) --- hive-c0re/src/lifecycle.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 2f6b622a..cb12cb55 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -337,24 +337,30 @@ pub async fn rebuild_no_meta( let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); if container_exists(name).await { // Existing container: preserve the prior running state across - // rebuild (closes #371). `nixos-container update` itself is - // already state-preserving — its only side effect on the running - // unit is a `systemctl reload container@` gated on - // `isContainerRunning`, so a stopped container stays stopped - // after the build + profile bump. We only need to recycle the - // outer unit when the container was running, so that any - // changes to `/etc/nixos-containers/.conf` (EXTRA_NSPAWN_FLAGS) - // or our systemd drop-ins (resource limits, stale-mount - // cleanup) are re-read by the next nspawn invocation. If a - // racing operator action started the container during the - // rebuild, that's their intent — don't override. + // rebuild (closes #371) and apply both the new system profile + // AND any `/etc/nixos-containers/.conf` / drop-in changes + // in a single start rather than `update`'s reload-then-outer- + // restart double-bounce. + // + // `nixos-container update` only runs `systemctl reload + // container@` when the container is up (per the + // `isContainerRunning` check in nixos-container.pl), so + // stopping first makes `update` boot-style: build + nix-env + // --set the new profile, skip the in-container + // switch-to-configuration, let the next `start` apply both + // the new profile and the new EXTRA_NSPAWN_FLAGS in one go. + // If the container was already stopped, `update` builds + sets + // the profile and we leave it stopped. let was_running = is_running(name).await; set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir)?; set_resource_limits(&container)?; systemd_daemon_reload().await?; + if was_running { + run(&["stop", &container]).await?; + } run(&["update", &container, "--flake", &flake_ref]).await?; if was_running { - run(&["restart", &container]).await + run(&["start", &container]).await } else { Ok(()) }