wip(#3001): convert remaining unblocked call sites; sweep docs

21 of 28 non-test call sites now insert directly. power.rs compiles.
The only remaining errors are server.rs's 5, which are blocked: those
sites feed the returned id into HostResponse::queued -> `queued_dags`,
a wire field hivectl polls via QueueDag. Removing the container without
answering that breaks hivectl's wait/progress loop; asked on the issue.

Also swept the deleted symbol out of prose, not just code:
- docs/coordinator.md: "the submit layer (job_queue/submit.rs)" ->
  the power layer (job_queue/power.rs), and "submits" -> "inserts".
- templates.rs module doc: points at super::power for the power ops.
- lifecycle_ops.rs module doc: says which path each op takes now.
- mod.rs's insert_group comment restated the open issue verbatim
  ("a DAG is addressed by its container node, which submit inserts
  itself"). Replaced with what is actually true for that path.

Dashboard behaviour deltas worth review: insert failures are now
logged per agent instead of swallowed, and UPDATE-ALL emits one queue
snapshot after the loop rather than one per agent.
This commit is contained in:
atlas 2026-08-04 11:27:26 +02:00 committed by mara
commit f04a0cee92
6 changed files with 54 additions and 65 deletions

View file

@ -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

View file

@ -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<AppState>) -> 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()
}

View file

@ -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:

View file

@ -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}"))?;

View file

@ -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;

View file

@ -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;