hive-c0re: push HelperEvent::Spawned as a todo, not a broker message

Follow-up from #2955 (mara: 'make core able to give agent a todo').
First migration slice: Spawned was pure FYI-check-when-convenient
material, not something needing an immediate turn.

Coordinator::push_todo/push_todo_submitter do a best-effort live dial
of the target agent's own hive-agent-sock (hive_host_sock::agent_todo_
socket), sending the exact UpsertTodo request in-container producers
(matrix/bash/forge-notify) already send. Push, not queue: agent
offline (socket absent) or dial failure is a silent no-op, no retry,
no fallback delivery -- matches mara's 'not available if offline'
call exactly.

HelperEvent::Spawned removed entirely (enum variant + all 3 call
sites migrated: handle_spawn's two arms, finish_approval's Spawn
approval-kind arm) rather than kept alongside a translation layer --
per mara's correction on the first design attempt, migrating the
producer means deleting the old path, not bridging it.

Verified: cargo build/clippy/test -p hive-c0re -p hive-host-sock
-p hive-sh4re clean (318 tests), nix fmt clean.
This commit is contained in:
damocles 2026-08-02 22:47:47 +02:00 committed by mara
commit d77f81cd1e
8 changed files with 150 additions and 37 deletions

View file

@ -1355,12 +1355,91 @@ impl Coordinator {
/// failure — fall back to the root agent, preserving the prior
/// always-root behaviour.
pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::HelperEvent) {
let target = self
.approvals
let target = self.submitter_or_manager(approval_id);
self.notify_agent(&target, event);
}
/// Shared resolution for "which agent should hear about approval
/// `approval_id`" — the authenticated socket caller at submit time,
/// falling back to the manager for legacy rows with no recorded
/// submitter (or any lookup failure). Used by both `notify_submitter`
/// and `push_todo_submitter` so the fallback rule lives in one place.
fn submitter_or_manager(&self, approval_id: i64) -> String {
self.approvals
.submitter_of(approval_id)
.unwrap_or_default()
.unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned());
self.notify_agent(&target, event);
.unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned())
}
/// Push a todo directly into `agent`'s in-container todo store — a
/// best-effort *live* dial of its `hive-agent-sock` socket
/// (`hive_host_sock::agent_todo_socket`), same `UpsertTodo` request
/// shape the in-container producers (matrix/bash/forge-notify)
/// already send. This is the migration target for `HelperEvent`
/// variants that are pure "FYI, check when convenient" notices: the
/// event stops being a broker `Message` (which always drives an
/// immediate turn) and becomes a todo instead, with the same
/// dedup-by-key semantics as any other producer.
///
/// Deliberately push, not queue: if the agent's container is down
/// (socket file absent) or the dial otherwise fails, this is a
/// silent no-op (logged at `debug`/`warn`) — no retry, no fallback
/// delivery. An agent that's down doesn't need a todo about
/// something it'll never see appear this way; whatever mechanism
/// resurfaces its state on the next boot is unrelated to this path.
pub async fn push_todo(
&self,
agent: &str,
subsystem: &str,
key: Option<String>,
summary: String,
source: Option<String>,
) {
let Ok(ident) = hive_types::Ident::parse(agent) else {
tracing::warn!(%agent, "push_todo: not a valid agent ident, skipping");
return;
};
let path = hive_host_sock::agent_todo_socket(&ident);
if !path.exists() {
tracing::debug!(%agent, path = %path.display(), "push_todo: agent socket not present (offline?), skipping");
return;
}
let req = hive_agent_sock::Request::UpsertTodo {
subsystem: subsystem.to_owned(),
key,
summary,
source,
};
match hive_sock_client::request::<_, hive_agent_sock::Response>(
&path,
&req,
hive_sock_client::Retry::None,
)
.await
{
Ok(hive_agent_sock::Response::Err { message }) => {
tracing::warn!(%agent, %message, "push_todo: agent rejected the todo");
}
Err(e) => {
tracing::warn!(%agent, error = ?e, "push_todo: dial failed");
}
Ok(_) => {}
}
}
/// `push_todo` to whichever agent submitted approval `approval_id` —
/// same resolution `notify_submitter` uses.
pub async fn push_todo_submitter(
&self,
approval_id: i64,
subsystem: &str,
key: Option<String>,
summary: String,
source: Option<String>,
) {
let target = self.submitter_or_manager(approval_id);
self.push_todo(&target, subsystem, key, summary, source)
.await;
}
/// Push a `HelperEvent` into an arbitrary agent's inbox. Encoded