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
This commit is contained in:
atlas 2026-07-26 16:05:13 +02:00 committed by mara
commit 7589f4c06c
3 changed files with 62 additions and 6 deletions

View file

@ -169,12 +169,22 @@ pub enum HookKind {
ResolveApproval,
/// Rebuild / perm-change: emit one `Rebuilt` manager event per agent.
EmitRebuilt,
/// Power-op: on a *cancelled* DAG, revert each agent's `wanted` intent.
/// Intent-writing power-op: on a *cancelled* DAG, revert each agent's
/// `wanted` intent. Only for templates that actually carry a `SetWanted`
/// head — reverting an intent a DAG never wrote invents one.
RevertIntent,
}
/// The terminal hook a DAG needs, from its template + approval id — or `None`
/// for a DAG with no terminal side effect (meta-update, boot, bare reconcile).
///
/// Restart is deliberately **not** a `RevertIntent` template. `restart_chain`
/// writes no `SetWanted` (it bounces the container and lets the tail
/// `Reconcile` converge to the agent's existing intent), so there is nothing
/// for a cancel to revert — and `revert_intent` writes the *observed* state,
/// which for a down-but-`wanted = Up` agent (crashed, or mid-bounce) would
/// flip it to `Offline` and keep it down. Cancelling a restart must leave the
/// intent exactly as it was found.
#[must_use]
pub fn terminal_hook(template: Template, approval_id: Option<i64>) -> Option<HookKind> {
if approval_id.is_some() {
@ -182,11 +192,7 @@ pub fn terminal_hook(template: Template, approval_id: Option<i64>) -> Option<Hoo
}
match template {
Template::Rebuild | Template::PermChange => Some(HookKind::EmitRebuilt),
Template::Start
| Template::Stop
| Template::GracefulStop
| Template::Restart
| Template::GracefulRestart => Some(HookKind::RevertIntent),
Template::Start | Template::Stop | Template::GracefulStop => Some(HookKind::RevertIntent),
_ => None,
}
}