feat(#2484): unify in-DAG growth on append_subgraph (drop append_node)

append_subgraph is the multi-node/multi-agent generalisation of the
single-node append_node, so the two in-DAG-growth channels collapse to
one: the Reconcile planner now emits its mechanical Start/Stop as a
single-node append_subgraph rooted on the reconcile node (stamping
claim.agent on the NodeSpec, which append_node inherited implicitly).

Removes NodeOutput.append_nodes + its scheduler drain loop and
JobQueue::append_node. No behaviour change — a channel unification.
This commit is contained in:
atlas 2026-07-15 20:21:39 +02:00
commit b87eac0a61
3 changed files with 48 additions and 104 deletions

View file

@ -154,57 +154,18 @@ impl JobQueue {
Ok(id)
}
/// Append a node into a *live* (non-terminal) DAG at runtime,
/// depending `AfterOk` on `dep_on` (the node that emitted it). Lets a
/// planner node — e.g. [`NodeKind::Reconcile`] — fan a mechanical
/// sub-step ([`NodeKind::Start`] / [`NodeKind::Stop`]) out as a
/// first-class node in the *same* DAG.
///
/// Must be called *before* the emitting node's [`Self::complete_node`]
/// so the DAG doesn't roll terminal with the new node still pending —
/// that keeps the lease-window transient held across the sub-step and
/// lets the appended node's `AfterOk` dep resolve as soon as the
/// emitter settles `Done`. No-op (returns `None`) if the DAG is gone.
pub fn append_node(&self, dag_id: u64, kind: NodeKind, dep_on: NodeId) -> Option<NodeId> {
let mut inner = self.inner.lock().expect("job_queue mutex poisoned");
let dag = inner.dags.iter_mut().find(|d| d.id == dag_id)?;
// The appended sub-step targets the same agent as the node that
// emitted it (a `Reconcile` fanning out its `Start`/`Stop` acts on
// the same container), so inherit `dep_on`'s agent.
let agent = dag.node(dep_on)?.agent.clone();
let new_id: NodeId = u32::try_from(dag.nodes.len()).unwrap_or(u32::MAX);
dag.nodes.push(Node {
id: new_id,
agent,
kind,
deps: vec![model::Dep {
on: dep_on,
when: DepWhen::AfterOk,
}],
state: State::Queued,
step: None,
build_log_id: None,
started_at: None,
finished_at: None,
error: None,
});
drop(inner);
self.notify.notify_one();
Some(new_id)
}
/// Append a whole *subgraph* into a live (non-terminal) DAG at runtime
/// — the multi-node, multi-agent generalisation of [`Self::append_node`].
/// Each [`NodeSpec`] carries its own `agent` and subgraph-relative `deps`
/// (indices into `nodes`); this rebases those onto the DAG's node-id
/// space (`id == index`, an invariant `append_node` also maintains) and
/// attaches every subgraph *root* — a node with no internal deps — to
/// `dep_on` with an `AfterOk` edge. Used by the startup sweep's
/// `MetaLock` to grow per-agent rebuild subgraphs into the same boot DAG
/// instead of fanning out child DAGs. Same call-*before*-`complete_node`
/// contract as `append_node` (so the DAG can't roll terminal with the
/// appended work still pending). Returns the new node ids; empty if the
/// DAG is gone or `nodes` is empty.
/// Append a whole *subgraph* into a live (non-terminal) DAG at runtime —
/// the single in-DAG-growth primitive. Each [`NodeSpec`] carries its own
/// `agent` and subgraph-relative `deps` (indices into `nodes`); this
/// rebases those onto the DAG's node-id space (`id == index`) and attaches
/// every subgraph *root* — a node with no internal deps — to `dep_on` with
/// an `AfterOk` edge. Used both for multi-node growth (the `MetaLock`
/// growing per-agent rebuild subgraphs into the same boot / meta-update
/// DAG instead of fanning out child DAGs) and the single-node case (a
/// `Reconcile` planner's `Start` / `Stop` as a one-node subgraph). Must be
/// called *before* the emitting node's [`Self::complete_node`] so the DAG
/// can't roll terminal with the appended work still pending. Returns the
/// new node ids; empty if the DAG is gone or `nodes` is empty.
pub fn append_subgraph(
&self,
dag_id: u64,