Commit graph hyperhive/hive-c0re/src/job_queue/mod.rs
Author SHA1 Message Date
atlas
a8728ac532 job_queue: move the jobs wire types to hive-host-sock
The DagView / NodeView / Source / State / PermPayload types only ever
travel on the host admin socket and the dashboard channels hive-c0re
serves off the same snapshot; their whole consumer set is hive-c0re,
hivectl and the socket protocol crate itself. Living in hive-sh4re made
the five other crates that depend on it carry job-queue types they never
name.

Pure move: git mv of the module plus the import sweep, no type changes.
hive-sh4re keeps its own chrono (wire_time still needs it).
2026-07-27 13:40:57 +02:00
atlas
5c4a637941 job_queue: delete the cancelled-power-op intent revert
The revert hook is dead by construction, so it can only ever be wrong.

DAG state `Cancelled` has exactly one producer: `JobQueue::cancel`, which
refuses unless every work node is still `Pending`. A cancel *cascade*
(some node failed, downstream cancelled) rolls up `Failed` instead —
`dag_rollup` short-circuits on any failed subtree node. So on a DAG that
reaches `Cancelled`, no node ever executed: the `SetWanted` head provably
never ran and `wanted` still reads whatever the operator last set it to.

There is therefore nothing to revert, and `revert_intent` did not revert
anything — it wrote `Wanted::from_running(observed)`, i.e. the agent's
*observed* state, over an intent the DAG never touched. Harmless when
observed already matched, silent corruption otherwise: cancel a queued
start for an agent that is down but `wanted = Up` (crashed, or caught
mid-bounce) and the intent flips to `Offline`, leaving it
deliberately-stopped as far as reconcile and crash-watch are concerned.

The hook made sense when `set_wanted` was a pre-submit side effect
written before the DAG ran; moving it into the DAG as a node left the
hook vestigial.

Drop `HookKind::RevertIntent`, `revert_intent`, and the power-op arm of
`terminal_hook` — start / stop / graceful-stop now settle with no
terminal hook, same as restart always did. The test asserts the general
statement across restart/stop/start x graceful x running: stop and start
carry a `SetWanted` head, and cancelling them still fires no hook.
2026-07-26 16:30:26 +02:00
atlas
7589f4c06c job_queue: stop reverting power intent on a cancelled restart
`terminal_hook` mapped `Restart` / `GracefulRestart` to `RevertIntent`, but
a restart never writes `wanted` — `restart_chain` deliberately has no
`SetWanted` head, so the tail `Reconcile` converges to the agent's existing
intent and a deliberately-stopped agent isn't forced up by a hive-wide
restart.

`revert_intent` writes `Wanted::from_running(observed)` unconditionally on a
cancelled DAG. So for an agent that is `wanted = Up` but currently down
(crashed, or caught behind another queued op), submitting a restart and then
cancelling it writes `wanted = Offline` — reverting an intent the DAG never
touched, to a value nobody asked for. Reconcile and crash-watch both then
read the agent as deliberately-stopped and leave it down.

It's invisible for a running agent, since `from_running(true)` equals the
intent already on file, which is why it went unnoticed. `cancel` only
succeeds while every node is still `Pending`, so the reachable window is
exactly "queued restart + observed != intent" — precisely when someone
restarts and then thinks better of it.

Drop both restart templates from the `RevertIntent` arm; they fall through
to no terminal hook, which is correct for a DAG that writes no intent.
Document the invariant on `HookKind::RevertIntent` and on `revert_intent`
itself: the hook writes *observed* state, so dispatching it for a template
with no `SetWanted` head doesn't restore an intent, it invents one.

Test covers all four restart shapes (graceful x running), asserting both
that the spec carries no `SetWanted` and that a cancelled restart dispatches
no hook, with a contrast arm pinning stop's revert in place.

Fixes hyperhive/hyperhive#2710
2026-07-26 16:30:26 +02:00
atlas
1db3cc32a1 job_queue: retire the now-off-wire step sub-step label
The `step` label was taken off the wire in #2661, when each deploy phase
became a first-class DAG node. Since then it has been written but never
read: `NodeRuntime` derives only `Debug, Default, Clone` — no serde — so
the field could not reach any client, and the only reads of it were the
dedup checks inside its own setters. This deletes the machinery.

Removed:

- `NodeRuntime.step`, `set_step`, `set_step_running`, and the
  `rt.step = None` clear in `complete_node`. `NodeRuntime` keeps its
  remaining `build_log_id` field (deliberately still a struct — collapsing
  it to a bare `Option<i64>` would churn every call site for no gain).
- `Ctx::step` and its ~15 call sites in `job_queue/exec.rs`. `Ctx` itself
  stays: it is the build-log sink, which `run_prebuild` and `run_swap`
  still use.
- `Coordinator::set_queue_step` and its 11 callers in `actions.rs`.
- `JobQueue::running_node_of`, reachable only from `set_queue_step`.
- `swap_update`'s `on_step` parameter and its one body call.
- The `set_step_only_on_running_and_signals_change` test.

Dropping the calls orphaned parameters, which are removed with their call
sites: `ctx` on ten executors that used it only as a step sink, and
`queue_entry_id` on `run_deploy_merge_verify` / `run_deploy_apply` /
`run_finalize_deploy` plus both `coord` and `queue_entry_id` on
`prepare_applied_target`. `run_deploy_tail` KEEPS its `queue_entry_id` —
that one has a genuine surviving use (the build-log link in the failure
comment posted to the PR).

One behavioural change, called out so it is not mistaken for a dropped
dashboard refresh: `Ctx::step` and `set_queue_step` each emitted a
`rebuild_queue_changed` snapshot when the label changed, and those
emissions go away with them. This is safe — the snapshot payload has no
step field, so those pushes carried nothing a client could observe. Real
state transitions still emit from the scheduler's claim and completion
paths, from `submit`, and from the three `actions.rs` sites. Net effect is
strictly fewer redundant SSE pushes.

Docs: `docs/coordinator.md` still listed `step` as a `NodeView` wire field
and `docs/web-ui/dashboard.md` documented a cyan `↳ <step>` sub-line under
each queue row. Neither has existed since #2661 — both corrected here, plus
the `job_queue/model.rs` module doc.

Not touched: `frontend/packages/dashboard/src/system-sections.css` has a
dead `.rqe-step` rule with no JS referencing it. Left for the frontend
owner rather than deleted here.

Closes: #2664
2026-07-26 15:24:35 +02:00
iris
cb936fe2fe feat(dashboard): show jobq node tree in build queue
Add NodeView::parent to the wire (hive-sh4re + hive-c0re dag_view), then
render the recursive parent/child tree in the dashboard build queue instead
of the previous flat chain/fan-out layout.

Wire change (hive-sh4re, hive-c0re):
- NodeView gains parent: Option<NodeId> (skip_serializing_if = None)
- dag_view() projects node.parent, filtering out the Dag container id
  (top-level work nodes become parent: None on the wire)

Frontend (builds.js):
- Replace nodeComponents + splitFanOut with buildNodeTree (uses parent
  edges directly) + topoSort helper (orders siblings by deps)
- renderTreeNode walks the tree depth-first, rendering indented rows
  with └─/├─ connectors and agent label per chip
- Flat chains and fan-out heuristics are gone; structure comes straight
  from the scheduler's parent axis

Closes: none (parent issue tracked in forge)
2026-07-26 03:14:52 +02:00
atlas
3429a8c5a6 job_queue: grow the rebuild subgraph from DeployApply (#2664)
The config-PR deploy's apply node still did the whole container rebuild
inline, through the last surviving `lifecycle::rebuild_no_meta` call. It
now merges, opens the two-phase meta deploy, and returns the ordinary
rebuild chain as a subgraph the scheduler grafts into the live DAG under
it. A new `FinalizeDeploy` node, gated on that graft, plants the deploy
tag and commits the staged lock.

Net effect: "did the agent come back up?" is answered by `Reconcile`
succeeding, the same way it is for every other rebuild, instead of by a
fused inline start — and each deploy phase is its own queue node, so the
dashboard shows which one is running.

The grafted nodes root on the apply node, so they land inside
`DeployWindow`'s subtree and re-enter the meta window and build slot it
already holds rather than deadlocking against them. The new happy-path
test runs on a one-slot queue specifically to pin that down.

`FinalizeDeploy`'s two git writes are fatal, deliberately: they are what
tells `DeployTail` a deploy confirmed good, so a node that merely warned
on them could report success while leaving the tail looking at the git
state of a failure — and the tail would then roll a good deploy back.
The trailing `meta::finalize_deploy` stays warn-only, since by then the
container already runs the new config.

The `failed/<id>` annotated tag moves into the tail, which is now the
only place holding a failed deploy. It reads the reason off the DAG via
a new `JobQueue::first_error`, and is gated on `main` having actually
moved — the rollback ref is parked *before* the merge, so its existence
alone does not mean a merge happened, and a pre-merge rejection must not
tag the previous, innocent head.

Removing the last inline rebuild orphaned a chain of now-dead code:
`rebuild_no_meta`, `container_exists`, `Coordinator::set_queue_build_log`
and `JobQueue::set_build_log_id_running`, all deleted here.
2026-07-26 02:28:03 +02:00
atlas
816006fd48 job_queue: hang the approval link off the deploy root
Non-derivable per-node payload rides the node that owns it. Tagging all
four deploy nodes with the approval id would render the same card four
times in `dag_view`.

Also fix `set_queue_step`'s doc comment, which claimed the DAG-id lookup
was exact because approval DAGs are single-node. They are not anymore;
what actually holds is that the chain is strictly sequential with the
root parked in Finishing, so at most one node is ever Running.
2026-07-25 22:55:02 +02:00
atlas
52dd9ede23 feat(#2591): add GET /api/build-log/<node_id> {,/raw} query endpoints
The raw-graph wire drops the inline build_log_id; the client fetches a
node's captured output on demand. Two handlers resolve node id -> log-row
id (JobQueue::build_log_id_of, now keyed by the wire u64) then delegate to
the existing get_full / raw handlers: /api/build-log/<node_id> serves the
BuildLogFull JSON ({stdout, stderr} + header), /raw serves text/plain.
404 when the node has no linked log.
2026-07-23 16:40:46 +02:00
atlas
02e2bf895e refactor(#2591): DagView carries host-computed timestamps + NodeView.has_log
Reconcile with mara + argus's review on the frontend PR (#2660):

- DagView regains started_at/finished_at (DateTime<Utc>), computed
  host-side as min/max over ALL subtree nodes (including the Done ones
  filtered off the wire). The client can't derive these — the
  earliest/only-started node is often Done and absent — so the backend
  sets them, per mara's call.
- NodeView gains has_log: bool = build_log_id.is_some(), the precise
  old 'node has a captured build log' guard so the dashboard only shows
  a log link for nodes that actually produce one.
2026-07-23 16:40:46 +02:00
atlas
51bcad1adb refactor(#2591): fix wire-shape consumers (server await_dags, tests)
- server::await_dags: a DAG is settled when gone from the snapshot (fully
  Done) or present with all nodes terminal; pending only with a non-terminal
  node (DagView no longer carries a rolled-up state).
- DagView::rollup_state() added to hive-sh4re — the shared node-set roll-up
  derivation every Rust consumer uses.
- JobQueue::build_log_id_of(node_id) — the node_id -> build_logs lookup the
  query endpoint will use; tests assert log-id via it now.
- tests: derive roll-up state; drop the off-wire step/build_log_id wire asserts.
2026-07-23 16:40:46 +02:00
atlas
c574948d7c refactor(#2591): read node lifecycle off the jobq Node; slim DagView build (WIP)
hive-c0re side of the raw-graph wire: dag_view now projects the slim
DagView, reading started_at/finished_at/error straight off the
hive_jobq Node (removes the node_rt double-write from #2645), excludes
Done nodes, and rides approval_id/inputs on the owning node. Template
moved into hive-c0re (model.rs) — no longer on the wire. Still WIP:
build-log endpoint + hivectl derive + compile fixes to follow.
2026-07-23 16:40:46 +02:00
atlas
03eb64cb5c feat(#2591): hive-jobq Node lifecycle — started/finished timestamps + failure reason
Node gains started_at/finished_at (chrono DateTime<Utc>, serialized
RFC 3339 on the wire per hive_sh4re::wire_time) plus error (String).
Graph::set_state self-stamps started_at on the first Running transition
and finished_at on the first terminal one, via an internal now_utc()
clock (keeps settle/complete signatures stable). Outcome::Failed(String)
carries the failure reason, set on the terminal transition.

hive-c0re complete_node builds Outcome::Failed(msg); its node_rt
side-table stays i64 for now (double-write) until #2637 reads the Node.

Toward #2637: the jobq graph becomes the source of truth for per-node
lifecycle so the queue can be sent to the client as-is.
2026-07-22 23:58:30 +02:00
atlas
0ab6b764be docs(#2591): fix stale Claim.agent doc + explain cancel's container roll-up (argus review) 2026-07-22 19:47:30 +02:00
atlas
600bc051e1 refactor(#2591): move the perm-change payload onto the WritePermFile node 2026-07-22 19:47:30 +02:00
atlas
be2dfa8cd3 refactor(#2591): make NodeKind the queue payload — drop JobPayload, agent into variants 2026-07-22 19:47:30 +02:00
atlas
2294cd4516 feat(#2591): auto-complete the DAG container + run terminal hooks inline 2026-07-22 19:47:30 +02:00
atlas
a78280feed refactor(#2591): model a DAG as a container node — grouping side-tables become graph walks 2026-07-22 19:47:30 +02:00
atlas
a5c321a1a0 feat(#2591): port hive-c0re job_queue onto the hive-jobq crate
Replace the in-tree scheduler with the domain-agnostic hive-jobq crate
(merged in #2615): parent-axis grouping + borrow/subtree-reservation
resource model + roll-up completion (State::Finishing).

Host adaptation:
- NodeSpec gains an explicit `parent` axis; templates declare grouping +
  sibling ordering directly (deps order execution, parent groups a subtree
  whose resource the descendants borrow).
- Rebuild is a nested two-root subtree: Prebuild (root, owns the build slot
  for the whole subtree, lease-exempt) -> StopForUpdate (child, owns the
  agent lease) -> Swap/PostSwap (children, borrow both); Reconcile is a
  separate top-level root (AfterAny Prebuild) so it survives the cancel-
  cascade of any failed step (recovery-start invariant) and converges to
  the persisted `wanted` on a fresh lease. This is the multi-root
  correction to the single-root-chain sketch: node0=root broke lease-
  exemption (hoisting the lease onto Prebuild) and recovery-reconcile
  (root failure cancels all children).
- Spawn / perm-change / power-ops (stop/start/restart) group-rooted the
  same way; per-agent power-op subgraphs stay independent roots so a
  multi-agent DAG runs them concurrently, each on its own lease.
- insert_group honours the explicit parent axis (no lease hoisting); the
  DAG terminal node deps AfterAny on every group root and runs once the
  whole op rolls up. Drop the old Graph::add_dep terminal wiring.

36/36 job_queue tests, full hive-c0re suite green, clippy --all-targets.
2026-07-22 19:47:30 +02:00
damocles
673aea4e50 fold hive-c0re module tree into the daemon binary + drop dead pub items surfaced by bin-only (#2513) 2026-07-16 10:43:08 +02:00
atlas
9fdadb99c0 feat(#2446): release a DAG's per-agent lease when that agent's subgraph is terminal
A per-agent lifecycle lease gates that agent's container globally across
concurrent DAGs, so it should be held for exactly as long as the agent's
work in a DAG is in flight, no longer. settle() previously freed every
lease a DAG held only at whole-DAG terminal, so a multi-agent DAG (a
hive-wide restart) kept agent A's container locked until B and C also
finished, blocking any other DAG wanting A.

Now free each agent's lease the moment its own subgraph within the DAG is
terminal (no live node still targets it), and drop that agent's dashboard
transient pill on the same edge via a new per-agent release channel. A
single-agent DAG is unaffected: its agent's subgraph goes terminal exactly
when the whole DAG does, so behaviour is identical.
2026-07-15 23:53:03 +02:00
atlas
b87eac0a61 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.
2026-07-15 20:23:11 +02:00
atlas
edf9fd036e feat(#2453): remove DAG parent_id now that every op is one DAG
With the meta-update cascade (#2476) and startup sweep (#2450) folded
into single DAGs that grow per-agent subgraphs via append_subgraph,
nothing links parent/child DAGs anymore — parent_id is dead.

hive-c0re: drop parent_id from Dag/DagSpec (+ the DagView copy); delete
append_children and cancel_children (no callers); simplify trim_history
(no more terminal-parent-with-live-children guard — a one-big-DAG is
terminal only when its whole graph settles); drop the rebuild() parent_id
param; QueueDag returns just the polled DAG (no fan-out children to
gather). hive-sh4re: drop the DagView.parent_id wire field.

frontend: a multi-step op is one DAG now, so renderRebuildQueue drops the
childrenOf/orphans cross-DAG grouping and renders each entry flat; its
per-agent subgraphs render as nodes within the one row (split by deps).
Removed the dead rqe-child style + isChild plumbing.

Docs + the child-DAG queue tests updated/removed to match.
2026-07-15 19:50:04 +02:00
atlas
4545dd312e feat(#2450): collapse the startup sweep into one inline DAG
The boot is now ONE DAG, assembled inline in submit_boot_tree — no boot_root
Noop anchor, no per-agent child DAGs, no display-only parent_id grouping: a
sweep MetaLock root (only when something is stale) that grows one rebuild
subgraph per stale agent into the same DAG (via append_subgraph, previous
commit), plus one Reconcile root per drifted agent (independent — a boot
reconcile needs no lock bump).

- submit_boot_tree builds the DagSpec inline; removed the single-use
  templates::{boot_root, startup_sweep} builders (inlined per the operator's
  "don't force single-use shapes into templates.rs" steer).
- fanout_specs simplified to the meta-update cascade path only — the startup
  sweep no longer fans out child DAGs, so its branch was dead.
- test: append_subgraph_roots_on_emitter_and_rebases_local_deps.

Vestigial after this (deliberately left as follow-ups, flagged in the PR):
NodeKind::Noop is now unconstructed (contained to hive-c0re, removable);
Template::StartupSweep is unconstructed but a hive-sh4re wire type
(frontend-coordinated removal).
2026-07-15 18:24:30 +02:00
atlas
b6defdeaaf feat(#2450): append_subgraph primitive + sweep MetaLock grows rebuilds in-DAG
First half of making the startup sweep one DAG. Adds the runtime
subgraph-append machinery and switches the sweep MetaLock from fanning out
child Rebuild DAGs to growing one rebuild subgraph per stale agent into the
same DAG:

- JobQueue::append_subgraph(dag_id, nodes, dep_on) — the multi-node,
  multi-agent generalisation of append_node: rebases a subgraph's local deps
  onto the DAG's id space and roots it on the emitting node.
- NodeOutput.append_subgraph: Vec<Vec<NodeSpec>> — the executor→scheduler
  channel for it; scheduler drains it before completing the emitting node
  (same ordering as append_nodes).
- run_meta_lock sweep branch returns the stale agents' rebuild_nodes
  subgraphs via append_subgraph instead of fanout.

Follow-up commit collapses submit_boot_tree (drop boot_root + per-agent
reconcile child DAGs) so the whole boot is one DAG built inline.
2026-07-15 18:24:30 +02:00
atlas
2a59f2f5fc refactor(#2441): move agent field from DAG onto Node; drop dedup
Agent was a single field on Dag/DagSpec, making a DAG structurally
one-agent — a multi-agent op could only ever be N separate DAGs. Move it
onto Node/NodeSpec (and the NodeView wire type), drop it from Dag/DagSpec
(and DagView): a DAG can now span agents.

- lifecycle lease keys on the node's agent, still globally exclusive per
  agent across all DAGs (Inner.leases unchanged in shape). A DAG holds one
  lease per distinct agent it touches; settle() frees each at DAG-terminal
  (per-agent-subgraph early release is a follow-up, only observable with
  multi-agent DAGs).
- transient guard keyed (dag_id, agent); cancel-revert + Rebuilt events
  walk TerminalDag.agents.
- submit-time dedup removed (a multi-agent DAG has no single agent to key
  on); every submit enqueues a fresh DAG. Whether dedup needs reintroducing
  is tracked in a follow-up sub-issue.
- templates gain a node(agent, kind, deps) helper stamping the agent onto
  every node; meta templates stamp "hyperhive".

Templates stay single-agent in this PR — behaviour is unchanged, only the
representation + wire shape. Multi-agent DAG emission (restart/restart-all/
broad stop+start as one DAG) and the SetWanted-as-a-node change are
follow-ups off #2439.
2026-07-14 21:58:40 +02:00
damocles
79d4c345bb feat(#2349): fan reconcile's start/stop out as first-class dag nodes 2026-07-12 03:10:18 +02:00
müde
2486251b32 fix: close second review round on the queue-routed CLI
- subvol upgrade waits for the queued stop DAG before migrating (was
  snapshotting + swapping state under a live bind mount) and for the
  restart job after
- history trim gets a 5-min grace for fresh terminals so broad
  stop/start waits can't miss a failed DAG evicted by the per-template
  cap (cap still applies past the grace)
- restart-all returns its DAG ids so hivectl actually waits
- hard stops await their agent DAGs (bounded) before infra goes down,
  restoring the agents-before-infra invariant
- hivectl wait uses node-level terminality so the after-any recovery
  reconcile is watched to completion; infra render errors no longer
  skip watching already-queued agent DAGs
- fold hive-bash-mcp's last local now_unix into wire_time
2026-07-06 22:57:28 +02:00
müde
b489454dc2 feat(hivectl): queue-routed lifecycle verbs with wait + DAG progress
every agent lifecycle verb on the admin socket (rebuild / restart /
restart-all / kill / stop / start) now submits job-queue DAGs and
returns their ids; hivectl polls the new HostRequest::QueueDag and
prints a live node-chain progress line per DAG (fan-out children
included), exiting non-zero on failure — --no-wait opts out. DagView
and the queue wire enums move to hive_sh4re::jobs (wire types live in
the shared crate); the last fused rebuild path (lifecycle::rebuild)
is gone. tracker: #2166
2026-07-06 22:30:49 +02:00
müde
dc6a37b29a style: import ordering + blank lines after now_unix sweep (treefmt) 2026-07-06 22:08:40 +02:00
müde
c84028ddcf refactor: single now_unix in hive_sh4re::wire_time
replaces 15 per-module copies (now_unix/now_secs) across hive-c0re and
hive-ag3nt; wire_time already owns the epoch-seconds convention
2026-07-06 21:58:32 +02:00
müde
084e12503c fix(hive-c0re): close review findings on the job-DAG queue
- deploy-window gate (meta::exclusive) + path-limited meta commits:
  a perm/lock/topology commit can no longer sweep an ApprovalDeploy's
  staged flake.lock and neuter abort_deploy (regression test included)
- cancel surfaces now buffer terminal roll-ups the scheduler drains,
  so a queued approval DAG cancelled by the operator resolves its
  approval instead of dangling, and cancelled power ops revert their
  wanted flip to the observed state
- hivectl restart / restart-all ride the queue (lease serialization,
  transient guard) and restart sets wanted=Up like the old kill+start
- exactly one Rebuilt event per rebuild DAG, emitted at terminal
- StopForUpdate pre-seeds a missing agent_power row from the pre-stop
  observation so a rebuild can't strand an unknown agent offline
- history trim keeps terminal fan-out parents with live children
- audit_log back on db::open; swarm.js badge for reconcile DAGs
2026-07-06 21:44:43 +02:00
müde
7946e03fde feat(hive-c0re): replace rebuild queue with generic job-DAG queue
jobs are now DAGs of primitive nodes (prebuild, stop-for-update, swap,
reconcile, signal, drain, ...) driven by one scheduler with N build
slots + per-agent lifecycle leases. per-agent power intent (wanted
up/offline) is durable in agent_power.sqlite; Reconcile nodes converge
observed state to it. kills the graceful-stop watcher thread, the
deferred-start follow-up, and the cascade pre-enqueue (fan-out on
MetaLock completion instead). tracker: #2166
2026-07-06 20:36:57 +02:00