//! 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; child `Rebuild` DAGs fan out on /// completion for every affected agent. MetaUpdate, /// First-deploy spawn (approval-driven). Spawn, /// Reserved for a future destroy integration. Destroy, /// Boot-time config sweep (hyperhive lock bump + stale-agent /// rebuild fan-out). StartupSweep, /// Mechanical stop + converge to `wanted = Up` (a restart). Restart, /// 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 root anchor: a single no-op node that groups this boot's /// `StartupSweep` + per-agent `Reconcile` child DAGs under one tree, so the /// dashboard renders the whole boot as one entry instead of N+1 rows. 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::StartupSweep => "startup_sweep", Template::Restart => "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, /// Cascade child of a `MetaUpdate` DAG's fan-out. MetaUpdate, /// Boot-time submission (sweep parent, boot reconciles). AutoUpdate, /// Cascade child of a `StartupSweep` DAG's fan-out. StartupSweep, /// 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::StartupSweep => "startup_sweep", 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, unique within its DAG. pub type NodeId = u32; /// 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, /// 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. DAG-level fields mirror the pre-DAG /// `QueueEntry` names (`kind` = template string, roll-up `state`); /// everything per-node appears exactly once, inside `nodes`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DagView { pub id: u64, pub agent: String, /// 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub parent_id: Option, 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub perm_payload: Option, pub nodes: Vec, }