//! Wire shapes of hive-c0re's job-DAG queue: what a queued job looks //! like on the dashboard SSE channel (`rebuild_queue_changed`), the //! `/api/state.rebuild_queue` snapshot, and the host admin socket's //! `QueueDag` polling surface (`hivectl`'s wait/progress loop). The //! queue *internals* — node kinds, dependency edges, scheduling state — //! live in `hive-c0re::job_queue`; these are the serialized views it //! produces. Semantics: `docs/coordinator.md::Job queue`. use serde::{Deserialize, Serialize}; /// What a DAG *means* — the request-level shape. Wire strings match /// the pre-DAG queue's `kind` values so dashboards key off the same /// tags. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Template { /// Rebuild one agent's container (prebuild → stop → profile-swap → /// reconcile). Rebuild, /// Bump meta flake locks; grows a rebuild subgraph per affected /// agent into the same DAG on completion. MetaUpdate, /// First-deploy spawn (approval-driven). Spawn, /// Reserved for a future destroy integration. Destroy, /// Mechanical stop + converge to `wanted = Up` (a restart). Restart, /// Signal → drain → mechanical stop → converge to `wanted = Up` — a /// graceful restart as one atomic DAG (drains the harness before the /// stop, same as `GracefulStop`, but then reconciles back up instead /// of staying down). GracefulRestart, /// Perm-file commit followed by the rebuild subgraph. PermChange, /// Quiesce the harness, drain, then stop (`wanted = Offline`). GracefulStop, /// Converge to `wanted = Up`. Start, /// Converge to `wanted = Offline`. Stop, /// Bare converge of observed power state to the persisted intent /// (boot reconcile). Reconcile, /// Boot-time config sweep as one DAG: a hyperhive lock bump that grows /// a rebuild subgraph per stale agent, plus a `Reconcile` per drifted /// agent — all in a single DAG (no anchor node, no child DAGs). Boot, } impl Template { #[must_use] pub fn as_str(self) -> &'static str { match self { Template::Rebuild => "rebuild", Template::MetaUpdate => "meta_update", Template::Spawn => "spawn", Template::Destroy => "destroy", Template::Restart => "restart", Template::GracefulRestart => "graceful_restart", Template::PermChange => "perm_change", Template::GracefulStop => "graceful_stop", Template::Start => "start", Template::Stop => "stop", Template::Reconcile => "reconcile", Template::Boot => "boot", } } } /// Where the submit request originated — drives the "why" chip on the /// dashboard. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Source { /// Operator action (dashboard button, CLI, manager tool). Manual, /// Meta-update cascade rebuild (grown into the meta-update DAG). MetaUpdate, /// Boot-time submission (the boot sweep DAG + boot reconciles). AutoUpdate, /// Crash recovery path (future use). CrashRecover, /// Operator approved a pending `Approval` row; `approval_id` on /// the DAG points back at the source row. Approval, } impl Source { #[must_use] pub fn as_str(self) -> &'static str { match self { Source::Manual => "manual", Source::MetaUpdate => "meta_update", Source::AutoUpdate => "auto_update", Source::CrashRecover => "crash_recover", Source::Approval => "approval", } } } /// Lifecycle state of a node — and, rolled up, of a DAG. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum State { Queued, Running, Done, Failed, Cancelled, } impl State { #[must_use] pub fn is_terminal(self) -> bool { matches!(self, State::Done | State::Failed | State::Cancelled) } } /// Kind-specific payload for `Template::PermChange` DAGs. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum PermPayload { /// Set the tool groups for one agent (`tool-groups.json`). ToolGroups { groups: Vec }, /// Set the capabilities for one agent (`capabilities.json`). Capabilities { caps: Vec }, /// Set both perm-types in one entry — the batch /// `POST /api/permissions` path. `None` leaves that file untouched; /// present fields commit together and rebuild once. Combined { groups: Option>, caps: Option>, }, } /// Node id. Carries the scheduler crate's globally-monotonic node id /// (`hive_jobq::NodeId`) verbatim on the wire — unique across all DAGs, not /// just within one. Consumers treat it opaquely (grouping + dep matching), /// so the widening from the old dag-local `u32` is transparent. pub type NodeId = u64; /// One node of a queued DAG, as serialized. Step labels, build-log /// links, errors, and timestamps are per-node; the DAG-level `state` /// is a roll-up. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeView { pub id: NodeId, /// The agent whose container (or meta repo, for `hyperhive` meta-level /// nodes) this node operates on. Agent is per-node — a single DAG can /// span multiple agents (e.g. a hive-wide restart), so there is no /// DAG-level agent field; consumers group by this. pub agent: String, /// Node primitive tag: `"prebuild"`, `"stop_for_update"`, /// `"swap"`, `"create"`, `"meta_lock"`, `"reconcile"`, `"signal"`, /// `"drain"`, `"write_dropin"`, `"write_perm_file"`, /// `"approval_deploy"`. pub kind: String, /// Ids of the nodes this one waits for. #[serde(default)] pub deps: Vec, pub state: State, #[serde(default, skip_serializing_if = "Option::is_none")] pub step: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub build_log_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub finished_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option, } /// A queued/running/recent DAG. `kind` = template string, roll-up /// `state`; everything per-node appears exactly once, inside `nodes`. /// There is no DAG-level `agent` — a DAG can span agents, so agent lives /// on each [`NodeView`]; consumers group nodes by `NodeView::agent`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DagView { pub id: u64, /// Template wire string — same values the old `kind` field used. pub kind: Template, /// Roll-up: `failed` if any node failed, else `running` / /// `queued` / `cancelled` / `done`. pub state: State, pub source: Source, pub reason: String, pub enqueued_at: i64, #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub finished_at: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub inputs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_id: Option, pub nodes: Vec, }