feat(#704): add <children> recipient sentinel — fan-out to direct descendants

This commit is contained in:
damocles 2026-05-31 20:41:34 +02:00 committed by mara
commit 36c683138d
5 changed files with 128 additions and 15 deletions

View file

@ -313,11 +313,38 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
}
}
/// Fan out one message to each recipient in `targets`. Skips the sender
/// itself. Returns a list of `"<agent>: <error>"` strings for any delivery
/// failures (empty = all good).
fn fan_out_send(
coord: &Arc<Coordinator>,
from: &str,
body: &str,
in_reply_to: Option<i64>,
targets: &[String],
) -> Vec<String> {
let mut errors = Vec::new();
for target in targets {
if target == from {
continue;
}
if let Err(e) = coord.broker.send(&Message {
from: from.to_owned(),
to: target.clone(),
body: body.to_owned(),
in_reply_to,
}) {
errors.push(format!("{target}: {e}"));
}
}
errors
}
/// Common Send handler shared between dispatch arms. Applies the
/// 4 KiB body cap, then routes broadcast (`to == "*"`) vs unicast
/// through their respective broker calls. Pulled out of `dispatch`
/// to keep that function under the clippy too-many-lines limit; the
/// behaviour is identical to inlining.
/// 4 KiB body cap, then routes broadcast (`to == "*"`) / children fan-out
/// (`to == "<children>"`) / unicast through their respective broker calls.
/// Pulled out of `dispatch` to keep that function under the clippy
/// too-many-lines limit; the behaviour is identical to inlining.
fn handle_send(
coord: &Arc<Coordinator>,
agent: &str,
@ -338,11 +365,25 @@ fn handle_send(
}
};
}
// Resolve magic-recipient sentinels (currently `<parent>`) against
// topology.json; no-op for ordinary names. Lets agents address
// structural roles without learning the label — runtime
// reparenting propagates for free. See `docs/conventions.md::
// Recipient sentinels`.
// `<children>`: fan out to every direct descendant of the sender per
// topology.json. Bypasses the allow-list check — structural fan-out
// targets are never user-listed peers. No-op (returns Ok) for leaf
// agents that have no children.
if to == hive_sh4re::CHILDREN_RECIPIENT {
let children = crate::topology::children_of(agent);
let errors = fan_out_send(coord, agent, body, in_reply_to, &children);
return if errors.is_empty() {
AgentResponse::Ok
} else {
AgentResponse::Err {
message: format!("children fan-out failed for agents: {}", errors.join(", ")),
}
};
}
// Resolve magic-recipient sentinels (`<parent>`) against topology.json;
// no-op for ordinary names. Lets agents address structural roles without
// learning the label — runtime reparenting propagates for free. See
// `docs/conventions.md::Recipient sentinels`.
let resolved = crate::topology::resolve_recipient(agent, to);
match coord.broker.send(&Message {
from: agent.to_owned(),