diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 383e2a23..9fec0107 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -131,6 +131,15 @@ pub struct Coordinator { /// agent is in this set — the inbound fence. Cleared when the agent /// reports `GracefulStopComplete` or the container is stopped. graceful_stop_pending: Mutex>, + /// Logical agent names that were running at the last broad-scope + /// `hivectl stop`. A subsequent broad-scope `hivectl start` restores + /// only this set (intersected with the requested scope) rather than + /// every configured container, so agents the operator intentionally + /// left stopped stay stopped. `None` when no broad stop has happened + /// since the last start (or since daemon boot) — start then falls + /// back to "start all". In-daemon memory only (hive-c0re survives + /// `hivectl stop`); host-reboot persistence is a separate follow-up. + last_stopped_running: Mutex>>, /// Unified wire-facing event channel feeding the dashboard SSE /// stream. Carries broker messages (mirrored from `broker.subscribe` /// by the forwarder task in `main.rs`) and dashboard-only mutation @@ -457,6 +466,7 @@ impl Coordinator { recent_transient: Mutex::new(HashMap::new()), recent_crashes: Mutex::new(HashMap::new()), graceful_stop_pending: Mutex::new(HashSet::new()), + last_stopped_running: Mutex::new(None), dashboard_events, event_seq: AtomicU64::new(0), meta_updates_active: AtomicU64::new(0), @@ -1157,6 +1167,20 @@ impl Coordinator { self.graceful_stop_pending.lock().unwrap().remove(name); } + /// Record the set of agents that were running at a broad-scope + /// `hivectl stop`, so the next broad-scope `start` restores exactly + /// this set. See the `last_stopped_running` field doc. + pub fn set_last_stopped_running(&self, agents: Vec) { + *self.last_stopped_running.lock().unwrap() = Some(agents); + } + + /// Take (and clear) the recorded broad-stop running set, if any. A + /// broad-scope `start` uses this to restore only the previously + /// running agents; `None` means "no record — start all". + pub fn take_last_stopped_running(&self) -> Option> { + self.last_stopped_running.lock().unwrap().take() + } + /// Set of agents whose transient was cleared within the last /// `grace` seconds — i.e. agents the operator just acted on, /// whose stop the crash watcher should NOT classify as a crash. diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 2248cb90..b774253e 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -101,10 +101,32 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { // graceful-stop queue, to re-expand it). let agents = scoped_agents(scope).await?; let infra = scoped_infra(scope); + // On a broad stop, remember which agents were actually + // running so a later broad `start` restores only those + // (not every configured container). A targeted `--agent` + // stop must not redefine the restore set. + if is_broad_scope(scope) { + let mut running = Vec::new(); + for a in &agents { + if lifecycle::is_running(a).await { + running.push(a.clone()); + } + } + coord.set_last_stopped_running(running); + } handle_stop(&coord, &agents, &infra, *graceful).await? } HostRequest::Start { scope } => { - let agents = scoped_agents(scope).await?; + let mut agents = scoped_agents(scope).await?; + // A broad start restores only the set recorded at the + // last broad stop, if any. No record (cold "bring the + // hive up", or a daemon restart since the stop) → start + // all. Targeted `--agent` start is never filtered. + if is_broad_scope(scope) + && let Some(prev) = coord.take_last_stopped_running() + { + agents.retain(|a| prev.contains(a)); + } let infra = scoped_infra(scope); handle_start(&agents, &infra).await? } @@ -328,10 +350,19 @@ async fn handle_start(agents: &[String], infra: &[InfraContainer]) -> Result` list: either the `agents` flag or a bare +/// "everything" scope. Broad scopes are the ones whose stop/start pair +/// drives the previously-running restore set (see `handle` Stop/Start +/// arms); a targeted `--agent` stop/start must not redefine it. +fn is_broad_scope(scope: &LifecycleScope) -> bool { + scope.agents || scope.is_everything() +} + async fn scoped_agents(scope: &LifecycleScope) -> Result> { use std::collections::BTreeSet; let mut set: BTreeSet = BTreeSet::new(); - if scope.agents || scope.is_everything() { + if is_broad_scope(scope) { for c in lifecycle::list().await? { let logical = c .strip_prefix(lifecycle::AGENT_PREFIX)