diff --git a/hive-c0re/src/job_queue/exec.rs b/hive-c0re/src/job_queue/exec.rs index de9e1f7d..fa303189 100644 --- a/hive-c0re/src/job_queue/exec.rs +++ b/hive-c0re/src/job_queue/exec.rs @@ -10,8 +10,7 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; -use super::Claim; -use hive_jobq::TerminalState; +use hive_jobq::{NodeId, TerminalState}; use super::model::NodeKind; use super::resource::Resource; @@ -43,22 +42,31 @@ pub const GRACEFUL_STOP_TIMEOUT: std::time::Duration = std::time::Duration::from /// future non-`Send`, which the scheduler's `tokio::spawn` rejects. So the /// growth executors below return *what to grow* and the declaration happens /// here, synchronously, between awaits. +/// +/// The node is identified by its own id + payload rather than by a `Claim` +/// side-struct: `kind` already carries the agent, and the DAG id is a +/// derived read (`JobQueue::dag_of`) the three arms that need it take +/// themselves. Nothing here needs a claim to exist as a type. pub(super) async fn run_node( coord: &Arc, job: super::Job, - claim: &Claim, + id: NodeId, + kind: &NodeKind, ) -> (super::Job, Result<()>) { + // The agent this node targets rides the payload — empty for the agentless + // container kinds (`MetaLock`, `Dag`), which never read it. + let agent = kind.agent(); // Every arm is `Result<()>`; the three that grow work declare into `job` // *synchronously*, after their own awaits have finished. Borrowing `&job` // inside an `.await` would make this future non-`Send` (see above), so the // growth executors return what to grow rather than taking the builder. - let result = match &claim.kind { - NodeKind::MetaSync { relock, .. } => run_meta_sync(coord, claim, *relock).await, - NodeKind::Prebuild { .. } => run_prebuild(claim).await, - NodeKind::Swap { .. } => run_swap(coord, claim).await, - NodeKind::PostSwap { .. } => run_post_swap(coord, claim).await, - NodeKind::Provision { .. } => run_provision(coord, claim).await, - NodeKind::Create { .. } => run_create(claim).await, + let result = match kind { + NodeKind::MetaSync { relock, .. } => run_meta_sync(coord, agent, *relock).await, + NodeKind::Prebuild { .. } => run_prebuild(agent, id).await, + NodeKind::Swap { .. } => run_swap(coord, agent, id).await, + NodeKind::PostSwap { .. } => run_post_swap(coord, agent).await, + NodeKind::Provision { .. } => run_provision(coord, agent).await, + NodeKind::Create { .. } => run_create(agent).await, NodeKind::MetaLock { sweep, fanout, @@ -70,7 +78,7 @@ pub(super) async fn run_node( super::templates::rebuild_nodes(&job, &agent, opts, None); } }), - NodeKind::Reconcile { .. } => run_reconcile(coord, claim).await.map(|sub| { + NodeKind::Reconcile { .. } => run_reconcile(coord, agent).await.map(|sub| { if let Some(kind) = sub { // `Start` / `Stop` declare the lease they run under. This node // is their parent and holds it, so the declaration is a @@ -81,38 +89,41 @@ pub(super) async fn run_node( let _ = job.node(kind).needs(lease); } }), - NodeKind::Start { .. } => run_start(coord, claim).await, - NodeKind::Stop { .. } => run_stop(coord, claim).await, - NodeKind::StopForUpdate { .. } => run_stop_for_update(coord, claim).await, + NodeKind::Start { .. } => run_start(coord, agent).await, + NodeKind::Stop { .. } => run_stop(coord, agent).await, + NodeKind::StopForUpdate { .. } => run_stop_for_update(coord, agent).await, NodeKind::Signal { .. } => { - run_signal(coord, claim); + run_signal(coord, agent); Ok(()) } - NodeKind::Drain { .. } => run_drain(coord, claim).await, - NodeKind::WriteDropin { .. } => run_write_dropin(coord, claim).await, - NodeKind::WritePermFile { .. } => run_write_perm_file(coord, claim).await, - NodeKind::Reparent { .. } => run_reparent(coord, claim).await, + NodeKind::Drain { .. } => run_drain(coord, agent).await, + NodeKind::WriteDropin { .. } => run_write_dropin(coord, agent).await, + // The payload rides the node and is destructured here, so the executor + // takes it directly instead of re-matching the kind behind a `bail!` + // that could never fire. + NodeKind::WritePermFile { payload, .. } => run_write_perm_file(coord, agent, payload).await, + NodeKind::Reparent { moves } => run_reparent(coord, moves).await, NodeKind::MergeVerify { approval_id, .. } => run_merge_verify(coord, *approval_id).await, NodeKind::DeployApply { approval_id, .. } => { run_deploy_apply(coord, *approval_id).await.map(|()| { - super::templates::deploy_rebuild_nodes(&job, claim.kind.agent(), *approval_id); + super::templates::deploy_rebuild_nodes(&job, agent, *approval_id); }) } NodeKind::FinalizeDeploy { approval_id, .. } => { run_finalize_deploy(coord, *approval_id).await } NodeKind::DeployTail { approval_id, .. } => { - run_deploy_tail(coord, claim, *approval_id).await + run_deploy_tail(coord, coord.job_queue.dag_of(id), agent, *approval_id).await } NodeKind::ResolveApproval { approval_id, outcome, - } => run_resolve_approval(coord, claim, *approval_id, *outcome).await, + } => run_resolve_approval(coord, coord.job_queue.dag_of(id), *approval_id, *outcome).await, NodeKind::EmitRebuilt { ok, .. } => { - run_emit_rebuilt(coord, claim, *ok); + run_emit_rebuilt(coord, agent, coord.job_queue.dag_of(id), *ok); Ok(()) } - NodeKind::SetWanted { up, .. } => run_set_wanted(coord, claim, *up), + NodeKind::SetWanted { up, .. } => run_set_wanted(coord, agent, *up), // The two nodes that carry no work of their own; completing either // lets it reach `Finishing` so the nodes under it start. // - `Dag`: pure grouping container. The DAG's terminal side effect, if @@ -133,12 +144,12 @@ pub(super) async fn run_node( /// since the work already happened and failing the tail would only misreport it. async fn run_resolve_approval( coord: &Arc, - claim: &Claim, + dag_id: Option, approval_id: i64, outcome: TerminalState, ) -> Result<()> { let reason = (outcome == TerminalState::Failed) - .then(|| coord.job_queue.first_error(claim.dag_id)) + .then(|| dag_id.and_then(|dag| coord.job_queue.first_error(dag))) .flatten(); crate::actions::resolve_approval_dag(coord, approval_id, outcome, reason.as_deref()).await; Ok(()) @@ -147,12 +158,12 @@ async fn run_resolve_approval( /// Emit this agent's `Rebuilt` manager event. `ok` is not computed — it is which /// of the tail pair the graph let run. The failure note comes from the DAG's /// first failing node, since the branch knows *that* it failed but not *why*. -fn run_emit_rebuilt(coord: &Arc, claim: &Claim, ok: bool) { +fn run_emit_rebuilt(coord: &Arc, agent: &str, dag_id: Option, ok: bool) { coord.notify_manager(&hive_sh4re::HelperEvent::Rebuilt { - agent: claim.agent.clone(), + agent: agent.to_owned(), ok, note: (!ok) - .then(|| coord.job_queue.first_error(claim.dag_id)) + .then(|| dag_id.and_then(|dag| coord.job_queue.first_error(dag))) .flatten(), sha: None, tag: None, @@ -167,7 +178,7 @@ fn run_emit_rebuilt(coord: &Arc, claim: &Claim, ok: bool) { /// warn-and-continue write, a failed write fails the node (cancel-downstream /// cancels the `Reconcile`) rather than letting it converge to a stale /// intent — that atomicity is the point of moving it into the DAG. -fn run_set_wanted(coord: &Arc, claim: &Claim, up: bool) -> Result<()> { +fn run_set_wanted(coord: &Arc, agent: &str, up: bool) -> Result<()> { let wanted = if up { crate::power::Wanted::Up } else { @@ -175,8 +186,8 @@ fn run_set_wanted(coord: &Arc, claim: &Claim, up: bool) -> Result<( }; coord .power - .set(&claim.agent, wanted) - .with_context(|| format!("set wanted={} for agent {}", wanted.as_str(), claim.agent))?; + .set(agent, wanted) + .with_context(|| format!("set wanted={} for agent {agent}", wanted.as_str()))?; Ok(()) } @@ -189,8 +200,7 @@ fn run_set_wanted(coord: &Arc, claim: &Claim, up: bool) -> Result<( /// Deliberately a separate node from the [`run_prebuild`] it feeds: that /// build takes minutes and only *reads* the store, so keeping the global /// window off it is what lets rebuilds of different agents overlap. -async fn run_meta_sync(coord: &Arc, claim: &Claim, relock: bool) -> Result<()> { - let name = &claim.agent; +async fn run_meta_sync(coord: &Arc, name: &str, relock: bool) -> Result<()> { // Runs while the agent is still up — the runtime dir and MCP listener // already exist. Use the pure path accessor; no need to re-register the // listener (event-driven: registered at start/create). @@ -217,15 +227,14 @@ async fn run_meta_sync(coord: &Arc, claim: &Claim, relock: bool) -> /// container is already down: its only purpose is to shrink the swap's /// downtime window, so a stopped agent (no uptime to preserve) doesn't /// pay the double eval — `Swap` builds inline instead. -async fn run_prebuild(claim: &Claim) -> Result<()> { - let name = &claim.agent; +async fn run_prebuild(name: &str, id: NodeId) -> Result<()> { // Warm the toplevel build only when the container is up — the whole // point of prebuild is to shrink the swap's downtime window. A // stopped agent has no uptime to preserve, so skip the (expensive) // eval and let the downstream `Swap` build inline. if crate::lifecycle::is_running(name).await { let flake_ref = format!("{}#{name}", crate::paths::meta_root().display()); - crate::lifecycle::prebuild_toplevel(name, &flake_ref, Some(claim.node_id.get())).await?; + crate::lifecycle::prebuild_toplevel(name, &flake_ref, Some(id.get())).await?; } Ok(()) } @@ -235,15 +244,13 @@ async fn run_prebuild(claim: &Claim) -> Result<()> { /// (rev marker, `Rebuilt` event, forge/matrix sync, kick, rescan). /// The recovery-start on failure is NOT here — the DAG's tail /// `Reconcile` runs after this node terminal ok *or* fail. -async fn run_swap(coord: &Arc, claim: &Claim) -> Result<()> { - let name = &claim.agent; +async fn run_swap(coord: &Arc, name: &str, id: NodeId) -> Result<()> { // Swap runs on an already-existing (stopped) container — runtime dir // and listener were created earlier. Pure path accessor suffices. let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); - let result = - crate::lifecycle::swap_update(name, &hive, &paths, Some(claim.node_id.get())).await; + let result = crate::lifecycle::swap_update(name, &hive, &paths, Some(id.get())).await; // On success the Ok-only bookkeeping tail (rev marker, forge/matrix // sync, kick, rescan, snapshot) runs in the sibling `PostSwap` node, // which deps `AfterOk(Swap)`. On failure `PostSwap` is cancel-cascaded @@ -262,8 +269,7 @@ async fn run_swap(coord: &Arc, claim: &Claim) -> Result<()> { /// means the profile swap succeeded. Store/forge/matrix work only — no nix /// build (build-slot-exempt); the agent lease taken at `Swap` is still held /// (the whole chain up to `Reconcile` is one agent's subgraph). -async fn run_post_swap(coord: &Arc, claim: &Claim) -> Result<()> { - let name = &claim.agent; +async fn run_post_swap(coord: &Arc, name: &str) -> Result<()> { if let Some(rev) = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) && let Err(e) = std::fs::write(crate::paths::applied_rev_marker(name), rev) { @@ -290,8 +296,7 @@ async fn run_post_swap(coord: &Arc, claim: &Claim) -> Result<()> { /// subvolume, and the meta `sync_agents` registration. Runs under the /// deploy window (it declares `Resource::MetaWindow`) so its commit can't /// land inside another node's staged deploy window. -async fn run_provision(coord: &Arc, claim: &Claim) -> Result<()> { - let name = &claim.agent; +async fn run_provision(coord: &Arc, name: &str) -> Result<()> { let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); @@ -305,8 +310,8 @@ async fn run_provision(coord: &Arc, claim: &Claim) -> Result<()> { /// dir creation and MCP listener registration are deferred to the tail /// `Reconcile` (`converge_start_preamble` + `register_agent`) so this /// node stays purely "create", not "create + start". -async fn run_create(claim: &Claim) -> Result<()> { - crate::lifecycle::create_only(&claim.agent).await?; +async fn run_create(name: &str) -> Result<()> { + crate::lifecycle::create_only(name).await?; Ok(()) } @@ -377,18 +382,17 @@ async fn run_meta_lock( /// guard) rides across it. /// Returns the mechanical node to fan out (`None` on a noop) rather than /// declaring it — the declaration has to happen outside any `.await`, see -/// [`run_node`]. `NodeKind` carries the agent it targets, so `claim.agent` is -/// stamped into the kind here. -async fn run_reconcile(coord: &Arc, claim: &Claim) -> Result> { - let name = &claim.agent; +/// [`run_node`]. `NodeKind` carries the agent it targets, so this node's agent +/// is stamped into the fanned-out kind here. +async fn run_reconcile(coord: &Arc, name: &str) -> Result> { let running = crate::lifecycle::is_running(name).await; let wanted = coord.power.get_or_seed(name, running)?; Ok(match reconcile_action(wanted, running) { ReconcileAction::Start => Some(NodeKind::Start { - agent: name.clone(), + agent: name.to_owned(), }), ReconcileAction::Stop => Some(NodeKind::Stop { - agent: name.clone(), + agent: name.to_owned(), }), ReconcileAction::Noop => { tracing::debug!(%name, wanted = wanted.as_str(), running, "reconcile: noop"); @@ -399,8 +403,7 @@ async fn run_reconcile(coord: &Arc, claim: &Claim) -> Result