diff --git a/hive-c0re/src/job_queue/mod.rs b/hive-c0re/src/job_queue/mod.rs index e81a3bb6..7ca0ce7f 100644 --- a/hive-c0re/src/job_queue/mod.rs +++ b/hive-c0re/src/job_queue/mod.rs @@ -289,8 +289,8 @@ impl JobQueue { #[must_use] pub fn first_error(&self, dag_id: u64) -> Option { let inner = self.lock(); - let container = container(&inner, dag_id)?; - inner.graph().first_error(container).map(ToOwned::to_owned) + let node = find_node(&inner, dag_id)?; + inner.graph().first_error(node).map(ToOwned::to_owned) } /// `(agent, label, takes_container_down)` for the live transient-pill set, @@ -379,38 +379,40 @@ impl JobQueue { .collect() } - /// One DAG's container node plus its live subtree, as generic wire - /// nodes — the `QueueNodes` polling surface behind `hivectl`'s - /// wait/progress loop. Sibling of [`Self::snapshot`] - /// (which serves the same graph through the typed `DagView`/`NodeView` - /// projection for the dashboard's `/api/state.rebuild_queue`), this one - /// goes through [`GraphWire::wire_snapshot`] instead — no `Done`-node - /// filtering, no roll-up field (the root's own `state` answers that, - /// see `hive_jobq_wire`'s doc comment). + /// A node plus its live subtree, as generic wire nodes — the + /// `QueueNodes` polling surface behind `hivectl`'s wait/progress loop. + /// Sibling of [`Self::snapshot`] (which serves the same graph through + /// the typed `DagView`/`NodeView` projection for the dashboard's + /// `/api/state.rebuild_queue`), this one goes through + /// [`GraphWire::wire_snapshot`] instead — no `Done`-node filtering, no + /// roll-up field (a node's own `state` answers that, see + /// `hive_jobq_wire`'s doc comment). Looks the id up by identity alone — + /// no assumption that it names a DAG container or a root; "just show + /// whatever the backend sends" for whatever id the caller asks about. /// - /// Empty when `dag_id` names no DAG container in the graph. Today that - /// only happens for a genuinely unknown id: nothing prunes the graph - /// yet (bounded-prune is a Stage-C follow-up, see [`visible_dags`]), so - /// a *completed* DAG's nodes keep riding here with a terminal `state` + /// Empty when `id` names no node in the graph. Today that only happens + /// for a genuinely unknown id: nothing prunes the graph yet + /// (bounded-prune is a Stage-C follow-up, see [`visible_dags`]), so a + /// *completed* DAG's nodes keep riding here with a terminal `state` /// rather than disappearing — callers watching for "done" should read /// the root's `state`, not emptiness. #[must_use] - pub fn dag_nodes(&self, dag_id: u64) -> Vec { + pub fn node_subtree(&self, id: u64) -> Vec { let inner = self.lock(); - let Some(root) = container(&inner, dag_id) else { + let Some(node) = find_node(&inner, id) else { return Vec::new(); }; - inner.graph().wire_snapshot([root]) + inner.graph().wire_snapshot([node]) } } -/// The container node of `dag_id` — the `NodeKind::Dag` root whose id equals -/// `dag_id`. `NodeId` is un-fabricable from a raw `u64`, so this is a search. -fn container(sched: &Sched, dag_id: u64) -> Option { - sched.graph().nodes().find_map(|n| { - (n.parent.is_none() && n.id.get() == dag_id && matches!(n.payload, NodeKind::Dag { .. })) - .then_some(n.id) - }) +/// The graph node whose id equals `id`, whatever its kind or depth. +/// `NodeId` is un-fabricable from a raw `u64`, so this is a search. +fn find_node(sched: &Sched, id: u64) -> Option { + sched + .graph() + .nodes() + .find_map(|n| (n.id.get() == id).then_some(n.id)) } /// Project a DAG into its wire [`DagView`]: a near-raw view of the diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index d187d0b8..4d22b800 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -160,7 +160,9 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { .collect(); HostResponse::dags(dags) } - HostRequest::QueueNodes { id } => HostResponse::nodes(coord.job_queue.dag_nodes(*id)), + HostRequest::QueueNodes { id } => { + HostResponse::nodes(coord.job_queue.node_subtree(*id)) + } HostRequest::List => HostResponse::list(lifecycle::list().await?), // The agents root is ours and not world-traversable, so this // question is only answerable on this side of the socket — diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index 560029a7..363a3442 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -214,13 +214,14 @@ pub enum HostRequest { /// `hivectl`'s wait/progress loop. A multi-step op is a single DAG /// (its whole graph in `nodes`). Result: [`HostResponse::dags`]. QueueDag { id: u64 }, - /// Fetch one job-queue DAG's container node plus its live subtree, as - /// generic `hive-jobq-wire` nodes — `hivectl`'s wait/progress loop. - /// Sibling of [`Self::QueueDag`]: same DAG, same - /// `id` (the container's own node id, what `queued_dags` already - /// carries), through the generic projection instead of the typed - /// `DagView`/`NodeView` (kept for `QueueDag`'s other consumer, - /// `/api/state.rebuild_queue`). Result: [`HostResponse::nodes`]. + /// Fetch one job-queue node plus its live subtree, as generic + /// `hive-jobq-wire` nodes — `hivectl`'s wait/progress loop. Sibling of + /// [`Self::QueueDag`]: same graph, same `id`, through the generic + /// projection instead of the typed `DagView`/`NodeView` (kept for + /// `QueueDag`'s other consumer, `/api/state.rebuild_queue`). No + /// assumption that `id` names a DAG container or root — whatever node + /// has that id, the backend hands back its subtree as-is. Result: + /// [`HostResponse::nodes`]. QueueNodes { id: u64 }, /// List pending approval requests. Pending, @@ -547,13 +548,13 @@ pub struct HostResponse { /// been evicted from the queue's history tail. #[serde(default, skip_serializing_if = "Option::is_none")] pub dags: Option>, - /// `QueueNodes` result — the requested DAG's container node plus its - /// live subtree, as generic `hive-jobq-wire` nodes. `None` for every - /// other request kind. An empty `Vec` means `id` names no live DAG in - /// the graph (today: an unknown id — see `JobQueue::dag_nodes`'s doc - /// comment for why a *completed* DAG's nodes don't vanish the same way - /// `QueueDag`'s do); callers should read the root node's `state` for - /// terminality, not emptiness. + /// `QueueNodes` result — the requested node plus its live subtree, as + /// generic `hive-jobq-wire` nodes. `None` for every other request kind. + /// An empty `Vec` means `id` names no live node in the graph (today: an + /// unknown id — see `JobQueue::node_subtree`'s doc comment for why a + /// *completed* DAG's nodes don't vanish the same way `QueueDag`'s do); + /// callers should read the root node's `state` for terminality, not + /// emptiness. #[serde(default, skip_serializing_if = "Option::is_none")] pub nodes: Option>, /// Free-form operator-facing output lines the client prints verbatim @@ -656,8 +657,8 @@ impl HostResponse { } } - /// `QueueNodes` result — the polled DAG's container + subtree, as - /// generic wire nodes. + /// `QueueNodes` result — the polled node + its subtree, as generic + /// wire nodes. #[must_use] pub fn nodes(nodes: Vec) -> Self { Self { diff --git a/hivectl/src/dag_progress.rs b/hivectl/src/dag_progress.rs index fe4c3d4c..917ba347 100644 --- a/hivectl/src/dag_progress.rs +++ b/hivectl/src/dag_progress.rs @@ -66,8 +66,8 @@ async fn wait_for_dags_plain(socket: &Path, ids: Vec) -> Result<()> { let nodes = resp.nodes.unwrap_or_default(); let Some(root) = find_root(&nodes) else { // Unknown id — nothing prunes the graph yet (see - // `JobQueue::dag_nodes`'s doc comment), so an id that - // resolves to no container never named a real DAG. A + // `JobQueue::node_subtree`'s doc comment), so an id that + // resolves to no node never named a real DAG. A // *completed* DAG's nodes keep riding here instead, with a // terminal root `state`, which is what the check below // watches for.