diff --git a/docs/coordinator.md b/docs/coordinator.md index 115d8ec2..b4e6b4c2 100644 --- a/docs/coordinator.md +++ b/docs/coordinator.md @@ -106,7 +106,7 @@ subgraph each (independent roots, run concurrently on their own leases), not N separate DAGs. **These are built dynamically from each agent's live running state** (an -async `lifecycle::is_running` read), so they live in `job_queue/submit.rs`, +async `lifecycle::is_running` read), so they live in `job_queue/power.rs`, not the pure/sync `templates.rs`. Per-agent shape rule: `stop`/`start` carry a head `SetWanted` (intent) — `restart` does not; the tail `Reconcile` (convergence guarantee — cheap, noops when already converged) is ALWAYS @@ -161,12 +161,12 @@ Notable collapses: Per-agent power *intent* — `wanted: Up | Offline` — is durable as the `agent_power` table in the coordinator DB (`hive-c0re/src/stores/power.rs`). `container_view` remains the observed *status*; `Reconcile` nodes converge the -two. Setting `wanted` is never a queued node: the submit layer -(`job_queue/submit.rs`) writes the row synchronously, then submits the DAG +two. Setting `wanted` is never a queued node: the power layer +(`job_queue/power.rs`) writes the row synchronously, then inserts the DAG whose `Reconcile` reads the fresh value — rapid toggles are last-writer-wins. Power toggles never commit to the meta repo. Every operator power surface — dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` — -rides the queue through that submit layer, so intent, lease serialization, +rides the queue through that power layer, so intent, lease serialization, and crash-watch suppression can't drift per surface; the only direct starts left are the root-agent bootstrap and infra containers (no lease, no harness). Cancelling a still-queued power DAG reverts `wanted` to the diff --git a/hive-c0re/src/dashboard/lifecycle_ops.rs b/hive-c0re/src/dashboard/lifecycle_ops.rs index 87e7c2d0..b4f399e0 100644 --- a/hive-c0re/src/dashboard/lifecycle_ops.rs +++ b/hive-c0re/src/dashboard/lifecycle_ops.rs @@ -1,11 +1,12 @@ //! Container lifecycle endpoints for the dashboard. //! //! Rebuild / restart / start / stop (hard + graceful) / update-all all -//! submit DAGs to the job queue (`job_queue::submit`), so each shows a -//! visible queued→running transient on the dashboard — a direct -//! sub-second start/stop only flashed the badge. Start/stop also -//! persist the agent's `wanted` power intent before submitting; the -//! DAG's `Reconcile` converges to it. Destroy delegates to +//! insert DAGs into the job queue — the power ops via +//! [`crate::job_queue::power`], the static shapes straight through +//! `JobQueue::insert` — so each shows a visible queued→running transient on +//! the dashboard; a direct sub-second start/stop only flashed the badge. +//! Start/stop also persist the agent's `wanted` power intent before +//! inserting; the DAG's `Reconcile` converges to it. Destroy delegates to //! `actions::destroy` (optionally purging). use axum::{ @@ -27,7 +28,6 @@ pub(super) struct GracefulParams { } use super::{AppState, Ident, error_response, guard_agent_name, strip_container_prefix}; -use crate::job_queue::{Source, submit}; use crate::{actions, lifecycle}; /// Queue a rebuild DAG for `name`. @@ -50,12 +50,13 @@ pub(super) async fn post_rebuild( if let Some(reject) = guard_agent_name(&state, &logical).await { return reject; } - submit::rebuild( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard ↻ R3BU1LD button".to_owned(), - ); + if let Err(e) = state.coord.job_queue.insert(|b| { + crate::job_queue::templates::rebuild(b, &logical, true); + Vec::new() + }) { + tracing::error!(agent = %logical, error = ?e, "rebuild: insert failed"); + } + state.coord.emit_rebuild_queue_snapshot(); (StatusCode::OK, "ok").into_response() } @@ -92,13 +93,11 @@ pub(super) async fn post_kill( // timeout fallback to a hard stop). The agent's lifecycle // lease keeps it from racing an in-flight rebuild for the same // agent, and per-node progress surfaces on the queue snapshot. - submit::graceful_stop( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard graceful stop".to_owned(), - ) - .await; + if let Err(e) = + crate::job_queue::power::stop_many(&state.coord, &[logical.clone()], true).await + { + tracing::error!(agent = %logical, error = ?e, "graceful stop: insert failed"); + } return (StatusCode::OK, "ok").into_response(); } // Manager is stoppable from the dashboard like any other @@ -111,13 +110,10 @@ pub(super) async fn post_kill( // `socket_server.rs::Request::Kill` stays in place: a // manager calling Kill on its own container is self-suicide // mid-call, not a legitimate operator action. - submit::stop( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard stop".to_owned(), - ) - .await; + if let Err(e) = crate::job_queue::power::stop_many(&state.coord, &[logical.clone()], false).await + { + tracing::error!(agent = %logical, error = ?e, "stop: insert failed"); + } (StatusCode::OK, "ok").into_response() } @@ -149,22 +145,18 @@ pub(super) async fn post_restart( return reject; } if params.graceful { - submit::graceful_restart( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard graceful restart".to_owned(), - ) - .await; + if let Err(e) = + crate::job_queue::power::restart_many(&state.coord, &[logical.clone()], true).await + { + tracing::error!(agent = %logical, error = ?e, "graceful restart: insert failed"); + } return (StatusCode::OK, "ok").into_response(); } - submit::restart( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard ↺ R3START button".to_owned(), - ) - .await; + if let Err(e) = + crate::job_queue::power::restart_many(&state.coord, &[logical.clone()], false).await + { + tracing::error!(agent = %logical, error = ?e, "restart: insert failed"); + } (StatusCode::OK, "ok").into_response() } @@ -226,13 +218,9 @@ pub(super) async fn post_start( return (StatusCode::OK, "ok").into_response(); } } - submit::start( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard start".to_owned(), - ) - .await; + if let Err(e) = crate::job_queue::power::start_many(&state.coord, &[logical.clone()]).await { + tracing::error!(agent = %logical, error = ?e, "start: insert failed"); + } (StatusCode::OK, "ok").into_response() } @@ -411,13 +399,14 @@ pub(super) async fn post_update_all(State(state): State) -> Response { else { continue; }; - submit::rebuild( - &state.coord, - &logical, - Source::Manual, - "manual via dashboard 🌀 UPDATE ALL".to_owned(), - ); + if let Err(e) = state.coord.job_queue.insert(|b| { + crate::job_queue::templates::rebuild(b, &logical, true); + Vec::new() + }) { + tracing::error!(agent = %logical, error = ?e, "update-all: insert failed"); + } } + state.coord.emit_rebuild_queue_snapshot(); (StatusCode::OK, "ok").into_response() } diff --git a/hive-c0re/src/dashboard/topology.rs b/hive-c0re/src/dashboard/topology.rs index d007f446..79ac83a2 100644 --- a/hive-c0re/src/dashboard/topology.rs +++ b/hive-c0re/src/dashboard/topology.rs @@ -21,7 +21,6 @@ use utoipa::ToSchema; use problem_details::ProblemDetails; use super::{AppState, error_problem}; -use crate::job_queue::{Source, submit}; /// `POST /api/topology/set-parent` body. `child` is required. /// `new_parent` may be: diff --git a/hive-c0re/src/job_queue/mod.rs b/hive-c0re/src/job_queue/mod.rs index 655ad636..4c5e74f2 100644 --- a/hive-c0re/src/job_queue/mod.rs +++ b/hive-c0re/src/job_queue/mod.rs @@ -161,9 +161,9 @@ fn insert_group( inner .insert_job(group_parent, |b| { declare(b); - // c0re names no handles: a DAG is addressed by its container node, - // which `submit` inserts itself, and nothing downstream looks an - // individual step up by id. + // A runtime-appended subgraph is addressed by the node that emitted + // it (`group_parent`), so this path names nothing. Callers that DO + // want a handle use `JobQueue::insert` and name the node there. Vec::new() }) .map_err(|e| anyhow::anyhow!("job_queue: graph insert failed: {e}"))?; diff --git a/hive-c0re/src/job_queue/power.rs b/hive-c0re/src/job_queue/power.rs index 1bb26b95..67d2650e 100644 --- a/hive-c0re/src/job_queue/power.rs +++ b/hive-c0re/src/job_queue/power.rs @@ -16,8 +16,9 @@ use std::sync::Arc; use hive_jobq::NodeId; -use super::JobBuilder; -use super::templates; +use super::resource::Resource; +use super::templates::rebuild_nodes; +use super::{JobBuilder, NodeKind}; use crate::coordinator::Coordinator; use crate::lifecycle; diff --git a/hive-c0re/src/job_queue/templates.rs b/hive-c0re/src/job_queue/templates.rs index 536093fc..cf110437 100644 --- a/hive-c0re/src/job_queue/templates.rs +++ b/hive-c0re/src/job_queue/templates.rs @@ -17,8 +17,8 @@ //! //! The hive-wide **power ops** (`stop` / `start` / `restart`) are NOT here: //! their per-agent shape depends on live running state (an async -//! `lifecycle::is_running` read), so `submit.rs` assembles them out of the -//! primitives this module exports ([`rebuild_nodes`]). +//! `lifecycle::is_running` read), so [`super::power`] assembles them out of +//! the primitives this module exports ([`rebuild_nodes`]). use hive_jobq::TerminalState;