jobq: key transient tombstones per pill, not per agent

Deletes `TransientState`, adds `transient_kind` to `TransientCleared`, and
fixes a crash misreport — three changes over the same functions.

`TransientState` was `RunningTransient` with `agent` dropped and
`takes_container_down` renamed; that rename was the only thing it did, and
its three consumers each read a disjoint subset. `transient_snapshot` now
returns `RunningTransient` directly.

`recent_transient` was keyed by agent alone and overwritten on each clear.
An agent can clear several pills in one grace window, so a `Prebuild`
(`takes_container_down = false`) landing after a `StopForUpdate` (`true`)
left the tombstone reading `false` and the crash watcher reported a
deliberate stop as a container crash. Keyed by `(agent, label)` now, with
`recent_transient_within` folding back per agent by OR — the same question
`crash_watch` asks of the active set.

`TransientCleared` gains the label for the same reason: a client holding
two open pills for one agent could not tell which one a clear referred to.

The out-of-band suppression guard has no node and so no label; it uses
`NO_NODE_LABEL`, angle-bracketed to stay out of the `NodeKind::as_str`
namespace.
This commit is contained in:
atlas 2026-08-03 15:00:08 +02:00
commit 7ef0e8c788
6 changed files with 110 additions and 53 deletions

View file

@ -157,7 +157,18 @@ pub enum DashboardEvent {
},
/// The matching lifecycle action resolved (success or failure).
/// Clients drop the spinner row.
TransientCleared { seq: u64, name: String },
///
/// `transient_kind` matches the `TransientSet` that opened this pill, and is
/// what makes the pair addressable: an agent can hold **several** pills at
/// once (the transient set tests status alone, so a lease-exempt `Prebuild`
/// and a lease-holding `StopForUpdate` are both live). Without it a client
/// holding two open pills for one agent cannot tell which one cleared, and
/// has to drop both or guess.
TransientCleared {
seq: u64,
name: String,
transient_kind: String,
},
/// One container row changed — new container appeared (post-spawn
/// finalise), an existing one flipped `running` / `needs_update` /
/// `sha`, etc. Clients upsert by `container.name`. Payload carries
@ -392,6 +403,7 @@ mod tests {
DashboardEvent::TransientCleared {
seq: 1,
name: "x".into(),
transient_kind: "rebuilding".into(),
},
DashboardEvent::ContainerRemoved {
seq: 1,