swarm: add a declared "paused" agent wanted state
mara (#4170): swarm-ui's wanted-state dropdown could only ever declare up/offline/destroy, with no way to swarm-declare the existing hive-local turn-loop pause (`hivectl agent pause|resume`). `AgentState::Paused` is not a fifth peer of Up/Offline/Destroyed on the power axis this enum otherwise answers — it's Up plus an orthogonal turn-loop pause. `hive-c0re`'s `workers::wanted` reconcile loop now decides the two axes independently (`decide` for power, the new `decide_pause` for the marker), so a stopped agent declared Paused converges with both a Start and a Pause in the same pass. Known, deliberate limitation: a Paused declaration on an agent this hive has never deployed only reaches Deploy this pass — writing the pause marker into a harness dir that may not exist yet was judged not worth the risk, so it converges on the next pass once the agent is present instead. swarm-ui's WantedMenu gains a fourth "paused" option (warning-tone badge). No separate "resume" entry — selecting "up" from a paused row already clears the marker via the same decide_pause path. Pause/resume marker writes go through one shared Coordinator::set_paused_by_name helper, used by both the interactive dashboard pause/resume handlers and this reconcile loop, instead of each duplicating the parse-name/write-marker/track-rescan shape. swarm-ui's "offline" and "paused" confirm dialogs share one confirmTarget state and one ConfirmDialog instead of two near-identical copies. Closes #4170
This commit is contained in:
parent
560f727797
commit
513554fe9a
6 changed files with 483 additions and 55 deletions
|
|
@ -91,6 +91,14 @@ pub enum AgentState {
|
|||
Up,
|
||||
/// Exists on the hive and is not running.
|
||||
Offline,
|
||||
/// Exists on the hive, is running, and its turn loop is parked — the
|
||||
/// swarm-declared counterpart of the hive-local `hivectl agent pause`
|
||||
/// marker. Orthogonal to the power axis `Up`/`Offline` express: a
|
||||
/// paused agent still has a running container (the web UI and MCP
|
||||
/// daemons stay reachable), it just drives no turns. See
|
||||
/// `hive-c0re::workers::wanted::decide_pause` for the convergence side
|
||||
/// of that split.
|
||||
Paused,
|
||||
/// Torn down entirely — the hive runs the destroy template once, then
|
||||
/// treats absence as agreement rather than re-running it. The key
|
||||
/// stays in the declared set with this state forever, on purpose: it's
|
||||
|
|
@ -111,6 +119,7 @@ impl AgentState {
|
|||
match self {
|
||||
AgentState::Up => "up",
|
||||
AgentState::Offline => "offline",
|
||||
AgentState::Paused => "paused",
|
||||
AgentState::Destroyed => "destroyed",
|
||||
}
|
||||
}
|
||||
|
|
@ -220,12 +229,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn the_known_states_decode_and_round_trip() {
|
||||
let doc =
|
||||
r#"{"agents":{"a":{"state":"up"},"b":{"state":"offline"},"c":{"state":"destroyed"}}}"#;
|
||||
let doc = r#"{"agents":{"a":{"state":"up"},"b":{"state":"offline"},"c":{"state":"destroyed"},"d":{"state":"paused"}}}"#;
|
||||
let decoded: HiveWanted = serde_json::from_str(doc).expect("decodes");
|
||||
assert_eq!(decoded.agents["a"].state, AgentState::Up);
|
||||
assert_eq!(decoded.agents["b"].state, AgentState::Offline);
|
||||
assert_eq!(decoded.agents["c"].state, AgentState::Destroyed);
|
||||
assert_eq!(decoded.agents["d"].state, AgentState::Paused);
|
||||
assert_eq!(serde_json::to_string(&decoded).expect("serialises"), doc);
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +246,10 @@ mod tests {
|
|||
/// the round-trip test above proving the happy path still works.
|
||||
#[test]
|
||||
fn an_unknown_state_fails_the_whole_declaration() {
|
||||
let doc = r#"{"agents":{"a":{"state":"up"},"c":{"state":"paused"}}}"#;
|
||||
// `"paused"` used to be this test's unknown example; it is a real
|
||||
// variant now, so `"sleeping"` takes its place as one this build
|
||||
// still does not know.
|
||||
let doc = r#"{"agents":{"a":{"state":"up"},"c":{"state":"sleeping"}}}"#;
|
||||
assert!(serde_json::from_str::<HiveWanted>(doc).is_err());
|
||||
}
|
||||
|
||||
|
|
@ -264,10 +276,18 @@ mod tests {
|
|||
/// to compile here rather than quietly going untested.
|
||||
#[test]
|
||||
fn as_str_matches_the_serde_spelling() {
|
||||
let every = [AgentState::Up, AgentState::Offline, AgentState::Destroyed];
|
||||
let every = [
|
||||
AgentState::Up,
|
||||
AgentState::Offline,
|
||||
AgentState::Paused,
|
||||
AgentState::Destroyed,
|
||||
];
|
||||
for state in every {
|
||||
match state {
|
||||
AgentState::Up | AgentState::Offline | AgentState::Destroyed => {}
|
||||
AgentState::Up
|
||||
| AgentState::Offline
|
||||
| AgentState::Paused
|
||||
| AgentState::Destroyed => {}
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::to_string(&state).expect("serialises"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue