//! Request-level submit API — the surface the dashboard POST handlers, //! the MCP socket handlers, and `hivectl` paths call. The durable //! `wanted` power intent is now written by a `SetWanted` DAG node at the //! head of each power-op template (not a pre-submit side effect); the //! only submit-time logic left is the stale-start *shape* decision //! (`start` vs `stale_start`). Every helper emits a fresh queue snapshot //! so the dashboard shows the new DAG immediately. use std::sync::Arc; use super::{Source, templates}; use crate::coordinator::Coordinator; fn submit_and_emit(coord: &Arc, spec: super::DagSpec) -> u64 { let id = coord .job_queue .submit(spec) .expect("template-built dag specs are acyclic"); coord.emit_rebuild_queue_snapshot(); id } /// Manual/approval-independent rebuild (always relocks the agent's /// meta input — cascade children are built by the scheduler's fan-out /// instead of this surface). pub fn rebuild(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { submit_and_emit(coord, templates::rebuild(agent, source, reason, None, true)) } /// Restart: mechanical stop + converge to `wanted = Up`. The intent /// write is the template's head `SetWanted(Up)` node — it matters when /// `wanted` drifted `Offline` under a running agent (an operator asking /// for a restart plainly wants it running, not a stop). pub fn restart(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { submit_and_emit(coord, templates::restart(agent, source, reason)) } /// Start: `SetWanted(Up)` (a DAG node now) then reconcile. A stale rev /// marker upgrades the start to a rebuild-then-start (`stale_start`, whose /// tail `Reconcile` does the start) so the container always comes up on /// current derivations — the old fast-lane `run_start` upgrade, still a /// submit-time *shape* decision (which template), while the intent write /// itself is now the template's head node. pub fn start(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { let stored = std::fs::read_to_string(crate::paths::applied_rev_marker(agent)).ok(); let stale = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) .is_some_and(|rev| stored.as_deref() != Some(rev.as_str())); if stale { tracing::info!(%agent, "start: rev stale — upgrading to rebuild+start"); return submit_and_emit( coord, templates::stale_start(agent, source, format!("{reason} (stale — rebuild+start)")), ); } submit_and_emit(coord, templates::start(agent, source, reason)) } /// Hard stop: `SetWanted(Offline)` (a DAG node now) then reconcile (kill + /// unregister + `Killed` event). pub fn stop(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { submit_and_emit(coord, templates::stop(agent, source, reason)) } /// Graceful stop: signal → drain → reconcile (the actual stop). The head /// `SetWanted(Offline)` node writes the intent as part of the DAG. pub fn graceful_stop(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { submit_and_emit(coord, templates::graceful_stop(agent, source, reason)) } /// Graceful restart: signal → drain → mechanical stop → reconcile (starts /// it back up) — one atomic DAG, no client-side "await the stop DAG then /// submit a start DAG" split. The head `SetWanted(Up)` node writes the /// intent as part of the DAG. pub fn graceful_restart( coord: &Arc, agent: &str, source: Source, reason: String, ) -> u64 { submit_and_emit(coord, templates::graceful_restart(agent, source, reason)) } /// Perm change: commit the JSON file(s) then rebuild. pub fn perm_change( coord: &Arc, agent: &str, source: Source, reason: String, payload: super::PermPayload, ) -> u64 { submit_and_emit( coord, templates::perm_change(agent, source, reason, payload), ) } /// Meta-input lock bump; cascade rebuilds fan out on completion. pub fn meta_update( coord: &Arc, inputs: Vec, source: Source, reason: String, ) -> u64 { submit_and_emit(coord, templates::meta_update(inputs, source, reason, None)) }