From 1e325c84f26a9f90dca8f7220e53c12a0c552f64 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 20 May 2026 17:06:16 +0200 Subject: [PATCH] fix: rebuild containers when meta flake changes, not only on hyperhive rev Closes #78 --- hive-c0re/src/auto_update.rs | 45 ++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 3acc674..c0a007c 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -38,6 +38,42 @@ pub fn current_flake_rev(hyperhive_flake: &str) -> Option { .map(|p| p.display().to_string()) } +/// Read the current git HEAD of the meta flake at +/// `/var/lib/hyperhive/meta`. Returns `None` when the repo does not exist +/// or `git rev-parse HEAD` fails (non-path flake, first-boot before +/// `sync_agents` has run, etc.). Callers treat `None` as "unknown" and +/// skip the meta-rev component of the combined marker. +#[must_use] +pub fn current_meta_rev() -> Option { + let out = std::process::Command::new("git") + .args(["-C", "/var/lib/hyperhive/meta", "rev-parse", "HEAD"]) + .output() + .ok()?; + if !out.status.success() { + return None; + } + let rev = String::from_utf8(out.stdout).ok()?; + let rev = rev.trim().to_owned(); + if rev.is_empty() { + None + } else { + Some(rev) + } +} + +/// Combine the hyperhive package rev and the optional meta flake rev into +/// one opaque marker string stored on disk. Including the meta rev means a +/// `sync_agents` run that rewrites the meta flake (e.g. adding a new +/// `HIVE_CONTEXT_WINDOW_TOKENS_*` env var) is detected and triggers a +/// container rebuild on the next hive-c0re boot. +#[must_use] +pub fn combined_rev(hyperhive_rev: &str, meta_rev: Option<&str>) -> String { + match meta_rev { + Some(m) => format!("{hyperhive_rev}:{m}"), + None => hyperhive_rev.to_owned(), + } +} + /// Read the marker for `name` and return whether the recorded rev matches /// `current_rev`. Missing/unreadable marker counts as out-of-date. #[must_use] @@ -123,7 +159,10 @@ pub async fn rebuild_agent(coord: &Arc, name: &str, current_rev: &s /// the approval queue — manager is required infrastructure. Idempotent. pub async fn ensure_manager(coord: &Arc) -> Result<()> { let existing = lifecycle::list().await.unwrap_or_default(); - let current_rev = current_flake_rev(&coord.hyperhive_flake); + let flake_rev = current_flake_rev(&coord.hyperhive_flake); + let meta_rev = current_meta_rev(); + let current_rev = + flake_rev.as_deref().map(|f| combined_rev(f, meta_rev.as_deref())); if existing.iter().any(|c| c == MANAGER_NAME) { // Container exists already. If it predates the unified lifecycle // (no applied flake on disk) we must rebuild — otherwise it's @@ -174,13 +213,15 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { /// per-agent outcomes and continues past failures. Returns Ok even if some /// rebuilds failed — startup shouldn't be blocked by a broken agent. pub async fn run(coord: Arc) -> Result<()> { - let Some(current_rev) = current_flake_rev(&coord.hyperhive_flake) else { + let Some(flake_rev) = current_flake_rev(&coord.hyperhive_flake) else { tracing::info!( flake = %coord.hyperhive_flake, "auto-update: hyperhive_flake has no canonical path; skipping", ); return Ok(()); }; + let meta_rev = current_meta_rev(); + let current_rev = combined_rev(&flake_rev, meta_rev.as_deref()); tracing::info!(rev = %current_rev, "auto-update: scanning agents"); // Bump meta's hyperhive input up-front so the per-agent rebuilds