todos: reopen an acked keyed row when the caller says so
This commit is contained in:
parent
d5782965db
commit
1b72ed56ff
12 changed files with 158 additions and 45 deletions
|
|
@ -967,6 +967,7 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
|
|||
Some(format!("destroyed:{name}")),
|
||||
format!("agent '{name}' destroyed"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
// Container row disappeared — rescan so the dashboard fires
|
||||
|
|
|
|||
|
|
@ -1439,6 +1439,8 @@ impl Coordinator {
|
|||
/// (`let _ = coord.push_todo(...).await;`); callers that track a
|
||||
/// per-target delivery outcome (e.g. the scheduled-prompts worker's
|
||||
/// `last_result` column) use it instead of assuming success.
|
||||
/// `reopen_if_acked` forwards to `UpsertTodo` (see its doc comment) —
|
||||
/// `false` for every caller here except the scheduled-prompts worker.
|
||||
pub async fn push_todo(
|
||||
&self,
|
||||
agent: &str,
|
||||
|
|
@ -1446,6 +1448,7 @@ impl Coordinator {
|
|||
key: Option<String>,
|
||||
summary: String,
|
||||
source: Option<String>,
|
||||
reopen_if_acked: bool,
|
||||
) -> Result<(), String> {
|
||||
let Ok(ident) = hive_types::Ident::parse(agent) else {
|
||||
tracing::warn!(%agent, "push_todo: not a valid agent ident, skipping");
|
||||
|
|
@ -1461,6 +1464,7 @@ impl Coordinator {
|
|||
key,
|
||||
summary,
|
||||
source,
|
||||
reopen_if_acked,
|
||||
};
|
||||
match hive_sock_client::request::<_, hive_agent_sock::Response>(
|
||||
&path,
|
||||
|
|
@ -1482,7 +1486,10 @@ impl Coordinator {
|
|||
}
|
||||
|
||||
/// `push_todo` to whichever agent submitted approval `approval_id` —
|
||||
/// same resolution `notify_submitter` uses.
|
||||
/// same resolution `notify_submitter` uses. Every caller here is a
|
||||
/// one-shot approval-resolution notice, so `reopen_if_acked` is
|
||||
/// unconditionally `false` — there's no reconciler/event distinction
|
||||
/// to make for an event that only ever fires once.
|
||||
pub async fn push_todo_submitter(
|
||||
&self,
|
||||
approval_id: i64,
|
||||
|
|
@ -1492,7 +1499,7 @@ impl Coordinator {
|
|||
source: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let target = self.submitter_or_manager(approval_id);
|
||||
self.push_todo(&target, subsystem, key, summary, source)
|
||||
self.push_todo(&target, subsystem, key, summary, source, false)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ async fn run_emit_rebuilt(coord: &Arc<Coordinator>, agent: &str, dag_id: Option<
|
|||
Some(format!("rebuilt:{agent}")),
|
||||
summary,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -441,6 +442,7 @@ async fn run_stop(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
|
|||
Some(format!("killed:{name}")),
|
||||
format!("agent '{name}' killed"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
coord.rescan_containers_and_emit().await;
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
|
|||
Some(format!("spawned:{name}")),
|
||||
format!("agent '{name}' spawned"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
// Update tmpfiles.d so the new agent's dirs survive a reboot.
|
||||
|
|
@ -318,6 +319,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
|
|||
Some(format!("spawned:{name}")),
|
||||
format!("agent '{name}' spawn FAILED: {e:#}"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
return Err(e);
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ pub(super) async fn handle_kill(coord: &Arc<Coordinator>, agent: &str, name: &st
|
|||
Some(format!("killed:{name}")),
|
||||
format!("agent '{name}' killed"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
Response::Ok
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ async fn emit_login_transitions(
|
|||
Some(format!("logged_in:{agent}")),
|
||||
format!("agent '{agent}' logged in"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -174,6 +175,7 @@ async fn emit_login_transitions(
|
|||
Some(format!("needs_login:{agent}")),
|
||||
format!("agent '{agent}' needs login"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,14 @@ async fn tick(coord: &Arc<Coordinator>) {
|
|||
/// upsert-by-key dedup the same job a now-removed
|
||||
/// `has_pending_with_body` broker check used to do — collapsing a
|
||||
/// re-fire of the *same schedule* against a target that hasn't
|
||||
/// reviewed the last one yet — and does it more precisely (keyed on
|
||||
/// schedule identity, not on the body happening to be byte-identical).
|
||||
/// reviewed the last one yet, keyed on schedule identity rather than on
|
||||
/// the body happening to be byte-identical. `deliver_to_target` also
|
||||
/// passes `reopen_if_acked = true`, so this collapse only applies while
|
||||
/// the previous fire is genuinely still sitting there un-reviewed — once
|
||||
/// the target acks it, the *next* fire reopens the row and wakes again
|
||||
/// even with an identical body, instead of silently staying quiet
|
||||
/// forever — a recurring schedule with a static body used to wake its
|
||||
/// target once, ever, and then look dead.
|
||||
async fn fire_schedule(coord: &Arc<Coordinator>, schedule: &Schedule, now: i64) {
|
||||
let known: std::collections::HashSet<String> = known_agents().await;
|
||||
for target_row in &schedule.targets {
|
||||
|
|
@ -158,7 +164,9 @@ async fn fire_schedule(coord: &Arc<Coordinator>, schedule: &Schedule, now: i64)
|
|||
/// to push into there), a `push_todo` otherwise, keyed on the
|
||||
/// schedule's own identity (`schedule:{schedule_id}`) so a re-fire
|
||||
/// against a target that hasn't reviewed the last one collapses via
|
||||
/// `push_todo`'s own upsert-by-key dedup. Shared by the periodic
|
||||
/// `push_todo`'s own upsert-by-key dedup — but `reopen_if_acked = true`
|
||||
/// means that collapse only holds while unreviewed; once acked, the next
|
||||
/// fire reopens regardless of whether the body changed. Shared by the periodic
|
||||
/// `fire_schedule` tick and the manual `fire_now` dashboard action —
|
||||
/// the only difference between them is what each caller does with the
|
||||
/// `Result` (log/prefix and per-target `last_result`/`FireNowReport`
|
||||
|
|
@ -188,6 +196,14 @@ async fn deliver_to_target(
|
|||
Some(format!("schedule:{schedule_id}")),
|
||||
body.to_owned(),
|
||||
Some("scheduled".to_owned()),
|
||||
// Each fire is a distinct occurrence, not a restatement of
|
||||
// a persisting condition — an agent that already acked the
|
||||
// *previous* firing hasn't acked *this* one, so an acked
|
||||
// row must reopen even when the body is byte-identical
|
||||
// (the common case: most schedules don't vary their text
|
||||
// per fire). See `Todos::upsert`'s doc comment for the
|
||||
// full reconciler-vs-event rationale.
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue