From 29c7f64bd30b75b00f004bf01911873cb7e0dc58 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 3 Jun 2026 21:51:45 +0200 Subject: [PATCH] refactor(#1202): introduce HiveEnv + AgentPaths to reduce arg repetition --- Cargo.lock | 1 + hive-c0re/src/actions.rs | 73 +++++----------------------- hive-c0re/src/auto_update.rs | 43 +++-------------- hive-c0re/src/coordinator.rs | 74 ++++++++++++++++++++++++++++ hive-c0re/src/lifecycle.rs | 93 +++++++++--------------------------- hive-c0re/src/meta.rs | 23 ++++----- hive-c0re/src/migrate.rs | 12 +---- hive-c0re/src/server.rs | 47 +++--------------- 8 files changed, 131 insertions(+), 235 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3468417e..2fae8eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,6 +1247,7 @@ version = "0.1.0" dependencies = [ "anyhow", "hive-sh4re", + "libc", "rmcp", "schemars", "serde", diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 73d66bc3..4ed288f5 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -130,19 +130,9 @@ pub async fn run_approval_apply_commit( let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::ApplyCommit)?; let agent_dir = coord.ensure_runtime(&approval.agent)?; let applied_dir = Coordinator::agent_applied_dir(&approval.agent); - let claude_dir = Coordinator::agent_claude_dir(&approval.agent); - let notes_dir = Coordinator::agent_notes_dir(&approval.agent); coord.set_queue_step(queue_entry_id, "apply commit"); - let (result, terminal_tag, is_first_spawn) = run_apply_commit( - coord, - &approval, - &agent_dir, - &applied_dir, - &claude_dir, - ¬es_dir, - queue_entry_id, - ) - .await; + let (result, terminal_tag, is_first_spawn) = + run_apply_commit(coord, &approval, &agent_dir, &applied_dir, queue_entry_id).await; coord.set_queue_step(queue_entry_id, "forge push"); if let Err(e) = crate::forge::push_config(&approval.agent).await { tracing::warn!(agent = %approval.agent, error = ?e, "forge: push_config after apply failed"); @@ -215,32 +205,14 @@ pub async fn run_approval_spawn( ) -> Result<()> { let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::Spawn)?; let agent_dir = coord.ensure_runtime(&approval.agent)?; - let proposed_dir = Coordinator::agent_proposed_dir(&approval.agent); - let applied_dir = Coordinator::agent_applied_dir(&approval.agent); - let claude_dir = Coordinator::agent_claude_dir(&approval.agent); - let notes_dir = Coordinator::agent_notes_dir(&approval.agent); + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(&approval.agent, agent_dir); // Transient guard keeps the per-container "Spawning" pill lit while // the worker is doing the actual nixos-container create. Auto-clears // on the function's scope exit (success or panic). let _guard = coord.transient_guard(&approval.agent, TransientKind::Spawning); coord.set_queue_step(queue_entry_id, "lifecycle::spawn"); - let result = lifecycle::spawn( - &approval.agent, - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - &agent_dir, - &proposed_dir, - &applied_dir, - &claude_dir, - ¬es_dir, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &coord.agent_cpu_quota, - &coord.agent_memory_max, - ) - .await; + let result = lifecycle::spawn(&approval.agent, &hive, &paths).await; if result.is_ok() { coord.set_queue_step(queue_entry_id, "forge user"); if let Err(e) = crate::forge::ensure_user_for(&approval.agent).await { @@ -450,8 +422,6 @@ async fn run_apply_commit( approval: &hive_sh4re::Approval, agent_dir: &std::path::Path, applied_dir: &std::path::Path, - claude_dir: &std::path::Path, - notes_dir: &std::path::Path, queue_entry_id: Option, ) -> (Result<()>, Option, bool) { let id = approval.id; @@ -548,17 +518,7 @@ async fn run_apply_commit( ); } }; - if let Err(e) = crate::meta::sync_agents( - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &agents, - ) - .await - { + if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await { let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; return ( @@ -585,14 +545,12 @@ async fn run_apply_commit( // Step labels are emitted inside rebuild_no_meta via the callback so // the dashboard reflects actual phase progress rather than a static // "nixos-container update" label for the whole multi-minute window. + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf()); let build_result = lifecycle::rebuild_no_meta( &approval.agent, - agent_dir, - applied_dir, - claude_dir, - notes_dir, - &coord.agent_cpu_quota, - &coord.agent_memory_max, + &hive, + &paths, &|step| coord.set_queue_step(queue_entry_id, step), ) .await; @@ -722,16 +680,7 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul /// destroy). Idempotent — a no-op when nothing changed. async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> { let agents = lifecycle::agents_for_meta_listing().await?; - crate::meta::sync_agents( - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &agents, - ) - .await + crate::meta::sync_agents(&coord.hive_env(), &agents).await } pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> { diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index a0c5b329..f4589284 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -77,9 +77,8 @@ pub async fn rebuild_agent( let agent_dir = coord .ensure_runtime(name) .with_context(|| format!("ensure_runtime {name}"))?; - let applied_dir = Coordinator::agent_applied_dir(name); - let claude_dir = Coordinator::agent_claude_dir(name); - let notes_dir = Coordinator::agent_notes_dir(name); + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(name, agent_dir); // Suppress crash_watch during the stop+start window inside // lifecycle::rebuild. Dashboard rebuilds already do this via // lifecycle_action; this catches the auto-update scan + any @@ -87,18 +86,8 @@ pub async fn rebuild_agent( let guard = coord.transient_guard(name, crate::coordinator::TransientKind::Rebuilding); let result = lifecycle::rebuild( name, - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - &agent_dir, - &applied_dir, - &claude_dir, - ¬es_dir, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &coord.agent_cpu_quota, - &coord.agent_memory_max, + &hive, + &paths, &|step| coord.set_queue_step(queue_entry_id, step), ) .await; @@ -190,27 +179,9 @@ pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { } tracing::info!("manager container missing — spawning"); let runtime = coord.ensure_runtime(MANAGER_NAME)?; - let proposed = Coordinator::agent_proposed_dir(MANAGER_NAME); - let applied = Coordinator::agent_applied_dir(MANAGER_NAME); - let claude_dir = Coordinator::agent_claude_dir(MANAGER_NAME); - let notes_dir = Coordinator::agent_notes_dir(MANAGER_NAME); - lifecycle::spawn( - MANAGER_NAME, - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - &runtime, - &proposed, - &applied, - &claude_dir, - ¬es_dir, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &coord.agent_cpu_quota, - &coord.agent_memory_max, - ) - .await?; + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(MANAGER_NAME, runtime); + lifecycle::spawn(MANAGER_NAME, &hive, &paths).await?; if let Some(rev) = current_rev { let _ = std::fs::write(rev_marker_path(MANAGER_NAME), &rev); } diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index ea09b8f8..4e70f5da 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -143,6 +143,44 @@ pub struct Coordinator { shutdown_tx: watch::Sender, } +/// Hive-wide configuration that lifecycle and meta operations need. +/// Extracted from `Coordinator` so callers can pass a single struct +/// instead of repeating the same 6 arguments everywhere. +/// +/// Cloned from `Coordinator` via [`Coordinator::hive_env`]. All fields +/// are cheap to clone (small strings + small map); lifecycle ops are +/// infrequent enough that the copy cost is irrelevant. +#[derive(Clone, Debug)] +pub struct HiveEnv { + pub hyperhive_flake: String, + pub nixpkgs_flake: String, + pub nixpkgs_unstable_flake: String, + pub dashboard_port: u16, + pub operator_pronouns: String, + pub context_window_tokens: std::collections::HashMap, + /// Per-agent systemd `CPUQuota=` value (e.g. `"200%"`). + pub agent_cpu_quota: String, + /// Per-agent systemd `MemoryMax=` value (e.g. `"4G"`). + pub agent_memory_max: String, +} + +/// Per-agent filesystem paths that lifecycle operations write to. +/// Assembled from `Coordinator`'s static path helpers so callers +/// don't repeat the same `Coordinator::agent_*_dir(name)` calls. +#[derive(Clone, Debug)] +pub struct AgentPaths { + /// Runtime socket dir (tmpfs, recreated per boot). + pub agent_dir: PathBuf, + /// Manager-editable proposed config repo. + pub proposed_dir: PathBuf, + /// Hive-c0re-authoritative applied config repo. + pub applied_dir: PathBuf, + /// Claude OAuth credentials (survives purge boundary). + pub claude_dir: PathBuf, + /// Agent durable notes + forge token (survives purge boundary). + pub notes_dir: PathBuf, +} + /// Per-agent in-progress state that the dashboard surfaces between approve /// click and container ready. #[derive(Debug, Clone)] @@ -282,6 +320,42 @@ impl Coordinator { }) } + /// Snapshot the hive-wide configuration fields as a [`HiveEnv`]. + /// Pass the result to `lifecycle::spawn` / `rebuild` / `meta::sync_agents` + /// instead of threading the individual fields separately. + #[must_use] + pub fn hive_env(&self) -> HiveEnv { + HiveEnv { + hyperhive_flake: self.hyperhive_flake.clone(), + nixpkgs_flake: self.nixpkgs_flake.clone(), + nixpkgs_unstable_flake: self.nixpkgs_unstable_flake.clone(), + dashboard_port: self.dashboard_port, + operator_pronouns: self.operator_pronouns.clone(), + context_window_tokens: self.context_window_tokens.clone(), + agent_cpu_quota: self.agent_cpu_quota.clone(), + agent_memory_max: self.agent_memory_max.clone(), + } + } + + /// Assemble the per-agent filesystem paths for `name`. The caller + /// must supply `agent_dir` (from `ensure_runtime`) since that + /// creates the tmpfs entry on first call. All other paths are + /// derived statically from `name`. + /// + /// ```no_run + /// let paths = Coordinator::agent_paths(name, coord.ensure_runtime(name)?); + /// ``` + #[must_use] + pub fn agent_paths(name: &str, agent_dir: PathBuf) -> AgentPaths { + AgentPaths { + agent_dir, + proposed_dir: Self::agent_proposed_dir(name), + applied_dir: Self::agent_applied_dir(name), + claude_dir: Self::agent_claude_dir(name), + notes_dir: Self::agent_notes_dir(name), + } + } + /// Emit a `RebuildQueueChanged` snapshot event. Called from the /// queue mutation helpers (`enqueue` / `finish` / `cancel`-adjacent /// wrappers below) and the worker so every state transition diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 62a35721..77c009d5 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -6,6 +6,8 @@ use anyhow::{Context, Result, bail}; use hive_sh4re::priv_proto::BindMount; use tokio::process::Command; +use crate::coordinator::{AgentPaths, HiveEnv}; + /// Sub-agent container prefix. `nixos-container` caps the total container name /// at 11 chars (it gets encoded into network interface names), so the agent /// name itself can be at most `MAX_AGENT_NAME` chars. @@ -221,23 +223,7 @@ async fn port_collision(self_name: &str) -> Option { None } -#[allow(clippy::too_many_arguments, clippy::implicit_hasher)] -pub async fn spawn( - name: &str, - hyperhive_flake: &str, - nixpkgs_flake: &str, - nixpkgs_unstable_flake: &str, - agent_dir: &Path, - proposed_dir: &Path, - applied_dir: &Path, - claude_dir: &Path, - notes_dir: &Path, - dashboard_port: u16, - operator_pronouns: &str, - context_window_tokens: &std::collections::HashMap, - cpu_quota: &str, - memory_max: &str, -) -> Result<()> { +pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> { validate(name)?; if let Some(other) = port_collision(name).await { bail!( @@ -245,28 +231,19 @@ pub async fn spawn( agent_web_port(name) ); } - setup_proposed(proposed_dir, name).await?; - setup_applied(applied_dir, Some(proposed_dir), name).await?; - ensure_claude_dir(claude_dir)?; - ensure_state_dir(notes_dir)?; + setup_proposed(&paths.proposed_dir, name).await?; + setup_applied(&paths.applied_dir, Some(&paths.proposed_dir), name).await?; + ensure_claude_dir(&paths.claude_dir)?; + ensure_state_dir(&paths.notes_dir)?; // Meta flake gets the new agent's input + nixosConfiguration // before `nixos-container create` so the `--flake meta#` // ref resolves. let agents = agents_after_spawn(name).await?; - crate::meta::sync_agents( - hyperhive_flake, - nixpkgs_flake, - nixpkgs_unstable_flake, - dashboard_port, - operator_pronouns, - context_window_tokens, - &agents, - ) - .await?; + crate::meta::sync_agents(hive, &agents).await?; let container = container_name(name); priv_run("create", name).await?; - set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir).await?; - set_resource_limits(&container, cpu_quota, memory_max).await?; + set_nspawn_flags(&container, &paths.agent_dir, &paths.claude_dir, &paths.notes_dir).await?; + set_resource_limits(&container, &hive.agent_cpu_quota, &hive.agent_memory_max).await?; systemd_daemon_reload().await?; priv_run("start", name).await } @@ -389,21 +366,10 @@ pub async fn destroy(name: &str) -> Result<()> { Ok(()) } -#[allow(clippy::too_many_arguments, clippy::implicit_hasher)] pub async fn rebuild( name: &str, - hyperhive_flake: &str, - nixpkgs_flake: &str, - nixpkgs_unstable_flake: &str, - agent_dir: &Path, - applied_dir: &Path, - claude_dir: &Path, - notes_dir: &Path, - dashboard_port: u16, - operator_pronouns: &str, - context_window_tokens: &std::collections::HashMap, - cpu_quota: &str, - memory_max: &str, + hive: &HiveEnv, + paths: &AgentPaths, on_step: &(dyn Fn(&str) + Send + Sync), ) -> Result<()> { // Sync the meta flake (idempotent — no-op when the rendered @@ -412,21 +378,12 @@ pub async fn rebuild( // got added directly via `nixos-container create` outside // hive-c0re). let agents = agents_for_meta(None).await?; - crate::meta::sync_agents( - hyperhive_flake, - nixpkgs_flake, - nixpkgs_unstable_flake, - dashboard_port, - operator_pronouns, - context_window_tokens, - &agents, - ) - .await?; + crate::meta::sync_agents(hive, &agents).await?; // Then bump just this agent's input — picks up whatever // `applied//main` currently points at (deployed/). // Commits the lock if it changed. crate::meta::lock_update_for_rebuild(name).await?; - rebuild_no_meta(name, agent_dir, applied_dir, claude_dir, notes_dir, cpu_quota, memory_max, on_step).await + rebuild_no_meta(name, hive, paths, on_step).await } /// Container-level rebuild without touching the meta repo. Callers @@ -441,12 +398,8 @@ pub async fn rebuild( /// is not needed. pub async fn rebuild_no_meta( name: &str, - agent_dir: &Path, - applied_dir: &Path, - claude_dir: &Path, - notes_dir: &Path, - cpu_quota: &str, - memory_max: &str, + hive: &HiveEnv, + paths: &AgentPaths, on_step: &(dyn Fn(&str) + Send + Sync), ) -> Result<()> { validate(name)?; @@ -456,17 +409,17 @@ pub async fn rebuild_no_meta( agent_web_port(name) ); } - setup_applied(applied_dir, None, name).await?; - ensure_claude_dir(claude_dir)?; - ensure_state_dir(notes_dir)?; + setup_applied(&paths.applied_dir, None, name).await?; + ensure_claude_dir(&paths.claude_dir)?; + ensure_state_dir(&paths.notes_dir)?; let container = container_name(name); let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); if container_exists(name).await { // Rebuild strategy: stop-before-update + pre-build. // See `docs/coordinator.md::Container lifecycle`. let was_running = is_running(name).await; - set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir).await?; - set_resource_limits(&container, cpu_quota, memory_max).await?; + set_nspawn_flags(&container, &paths.agent_dir, &paths.claude_dir, &paths.notes_dir).await?; + set_resource_limits(&container, &hive.agent_cpu_quota, &hive.agent_memory_max).await?; systemd_daemon_reload().await?; if was_running { on_step("nix build"); @@ -537,8 +490,8 @@ pub async fn rebuild_no_meta( // See `docs/coordinator.md::Spawn path`. on_step("nixos-container create"); priv_run("create", name).await?; - set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir).await?; - set_resource_limits(&container, cpu_quota, memory_max).await?; + set_nspawn_flags(&container, &paths.agent_dir, &paths.claude_dir, &paths.notes_dir).await?; + set_resource_limits(&container, &hive.agent_cpu_quota, &hive.agent_memory_max).await?; systemd_daemon_reload().await?; on_step("nixos-container start"); priv_run("start", name).await diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 9c05baf9..d5795b79 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -10,6 +10,7 @@ use anyhow::{Context, Result, bail}; use tokio::process::Command; use tokio::sync::Mutex; +use crate::coordinator::HiveEnv; use crate::lifecycle; const META_ROOT: &str = "/var/lib/hyperhive/meta"; @@ -50,26 +51,18 @@ pub fn meta_dir() -> PathBuf { /// rendered contents differ from disk; an unchanged `flake.nix` is a /// no-op. #[allow(dead_code, clippy::implicit_hasher)] // first caller lands in a later commit -pub async fn sync_agents( - hyperhive_flake: &str, - nixpkgs_flake: &str, - nixpkgs_unstable_flake: &str, - dashboard_port: u16, - operator_pronouns: &str, - context_window_tokens: &std::collections::HashMap, - agents: &[AgentSpec], -) -> Result<()> { +pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> { let _guard = META_LOCK.lock().await; let dir = meta_dir(); std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?; let new_flake = render_flake( - hyperhive_flake, - nixpkgs_flake, - nixpkgs_unstable_flake, - dashboard_port, - operator_pronouns, - context_window_tokens, + &hive.hyperhive_flake, + &hive.nixpkgs_flake, + &hive.nixpkgs_unstable_flake, + hive.dashboard_port, + &hive.operator_pronouns, + &hive.context_window_tokens, agents, ); let flake_path = dir.join("flake.nix"); diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 9d168773..a0d5066b 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -76,17 +76,7 @@ pub async fn run(coord: &Arc) -> Result<()> { let agents = lifecycle::agents_for_meta_listing() .await .unwrap_or_default(); - if let Err(e) = meta::sync_agents( - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &agents, - ) - .await - { + if let Err(e) = meta::sync_agents(&coord.hive_env(), &agents).await { tracing::warn!(error = ?e, "migration: meta sync_agents failed"); } diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 2c815dea..d305baea 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -80,27 +80,9 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { HostRequest::Spawn { name } => { tracing::info!(%name, "spawn"); let agent_dir = coord.ensure_runtime(name)?; - let proposed_dir = Coordinator::agent_proposed_dir(name); - let applied_dir = Coordinator::agent_applied_dir(name); - let claude_dir = Coordinator::agent_claude_dir(name); - let notes_dir = Coordinator::agent_notes_dir(name); - match lifecycle::spawn( - name, - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - &agent_dir, - &proposed_dir, - &applied_dir, - &claude_dir, - ¬es_dir, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &coord.agent_cpu_quota, - &coord.agent_memory_max, - ) - .await + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(name, agent_dir); + match lifecycle::spawn(name, &hive, &paths).await { Ok(()) => { coord.notify_manager(&hive_sh4re::HelperEvent::Spawned { @@ -178,26 +160,9 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { HostRequest::Rebuild { name } => { tracing::info!(%name, "rebuild"); let agent_dir = coord.ensure_runtime(name)?; - let applied_dir = Coordinator::agent_applied_dir(name); - let claude_dir = Coordinator::agent_claude_dir(name); - let notes_dir = Coordinator::agent_notes_dir(name); - let result = lifecycle::rebuild( - name, - &coord.hyperhive_flake, - &coord.nixpkgs_flake, - &coord.nixpkgs_unstable_flake, - &agent_dir, - &applied_dir, - &claude_dir, - ¬es_dir, - coord.dashboard_port, - &coord.operator_pronouns, - &coord.context_window_tokens, - &coord.agent_cpu_quota, - &coord.agent_memory_max, - &|_| (), - ) - .await; + let hive = coord.hive_env(); + let paths = Coordinator::agent_paths(name, agent_dir); + let result = lifecycle::rebuild(name, &hive, &paths, &|_| ()).await; // Mirror auto_update::rebuild_agent — the manager wants // to know about every rebuild attempt regardless of // which surface triggered it, especially failures