manager: add optional agent param to GetLooseEnds

GetLooseEnds now takes agent: Option<String>:
- None   = manager's own loose ends (default; the bug fix)
- Some("*")    = hive-wide view (every approval/question/reminder)
- Some("name") = that agent's loose ends

The get_loose_ends MCP tool exposes this as an optional agent arg, so
the manager can still scan the whole swarm on demand. The web UI and
post-turn counts pass None (manager's own).
This commit is contained in:
iris 2026-05-20 21:42:21 +02:00 committed by Mara
parent 873d5a083d
commit d348ce885f
5 changed files with 50 additions and 22 deletions

View file

@ -334,7 +334,7 @@ fn now_unix() -> i64 {
async fn fetch_manager_post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) { async fn fetch_manager_post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
let threads = match client::request::<_, ManagerResponse>( let threads = match client::request::<_, ManagerResponse>(
socket, socket,
&ManagerRequest::GetLooseEnds, &ManagerRequest::GetLooseEnds { agent: None },
) )
.await .await
{ {

View file

@ -756,6 +756,18 @@ pub struct CancelLooseEndArgs {
pub id: i64, pub id: i64,
} }
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GetLooseEndsArgs {
/// Whose loose ends to list. Omit (or `null`) for your own — the
/// manager's: approvals you submitted + questions where you are
/// asker/target + your own pending reminders. Pass `"*"` for a
/// hive-wide view of EVERY pending approval, unanswered question,
/// and reminder across the swarm. Pass a specific agent name to
/// inspect just that agent's threads.
#[serde(default)]
pub agent: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct RequestApplyCommitArgs { pub struct RequestApplyCommitArgs {
/// Agent whose config repo the commit lives in (use `"hm1nd"` for the /// Agent whose config repo the commit lives in (use `"hm1nd"` for the
@ -1126,18 +1138,19 @@ impl ManagerServer {
} }
#[tool( #[tool(
description = "Hive-wide loose ends: EVERY pending approval + EVERY unanswered \ description = "List loose ends. By default returns your OWN — the manager's: \
question + EVERY pending reminder across the swarm. Use to scan for stalled \ pending approvals you submitted + unanswered questions where you are \
coordination questions sub-agents asked each other that nobody's answering, \ asker/target + your own pending reminders. Pass `agent: \"*\"` for a \
approvals stuck waiting on the operator, reminders piling up on an offline \ hive-wide scan (EVERY pending approval, unanswered question, and reminder \
agent, etc. No args. The sub-agent flavour only returns the agent's own \ across the swarm) use it to spot stalled coordination, e.g. questions \
threads; the manager flavour is unfiltered. Cancel any question or reminder \ sub-agents asked each other that nobody's answering. Pass `agent: \
\"<name>\"` to inspect one agent's threads. Cancel any question or reminder \
row via `cancel_loose_end` (manager bypasses the owner check)." row via `cancel_loose_end` (manager bypasses the owner check)."
)] )]
async fn get_loose_ends(&self) -> String { async fn get_loose_ends(&self, Parameters(args): Parameters<GetLooseEndsArgs>) -> String {
run_tool_envelope("get_loose_ends", String::new(), async move { run_tool_envelope("get_loose_ends", String::new(), async move {
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_sh4re::ManagerRequest::GetLooseEnds) .dispatch(hive_sh4re::ManagerRequest::GetLooseEnds { agent: args.agent })
.await; .await;
annotate_retries(format_loose_ends(resp), retries) annotate_retries(format_loose_ends(resp), retries)
}) })

View file

@ -409,7 +409,9 @@ async fn api_loose_ends(State(state): State<AppState>) -> Response {
Flavor::Manager => { Flavor::Manager => {
match client::request::<_, hive_sh4re::ManagerResponse>( match client::request::<_, hive_sh4re::ManagerResponse>(
&state.socket, &state.socket,
&hive_sh4re::ManagerRequest::GetLooseEnds, // Manager's own loose ends — the web page is the
// manager's page, not a hive-wide console.
&hive_sh4re::ManagerRequest::GetLooseEnds { agent: None },
) )
.await .await
{ {

View file

@ -396,12 +396,19 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
}, },
} }
} }
ManagerRequest::GetLooseEnds => match crate::loose_ends::for_agent(coord, MANAGER_AGENT) { ManagerRequest::GetLooseEnds { agent } => {
let result = match agent.as_deref() {
Some("*") => crate::loose_ends::hive_wide(coord),
Some(name) => crate::loose_ends::for_agent(coord, name),
None => crate::loose_ends::for_agent(coord, MANAGER_AGENT),
};
match result {
Ok(loose_ends) => ManagerResponse::LooseEnds { loose_ends }, Ok(loose_ends) => ManagerResponse::LooseEnds { loose_ends },
Err(e) => ManagerResponse::Err { Err(e) => ManagerResponse::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
}, }
}
ManagerRequest::CountPendingReminders => { ManagerRequest::CountPendingReminders => {
match coord.broker.count_pending_reminders_for(MANAGER_AGENT) { match coord.broker.count_pending_reminders_for(MANAGER_AGENT) {
Ok(count) => ManagerResponse::PendingRemindersCount { count }, Ok(count) => ManagerResponse::PendingRemindersCount { count },

View file

@ -792,12 +792,18 @@ pub enum ManagerRequest {
#[serde(default)] #[serde(default)]
file_path: Option<String>, file_path: Option<String>,
}, },
/// Hive-wide loose-ends view: EVERY pending approval + EVERY /// Loose-ends view for the manager surface. The optional `agent`
/// unanswered question in the swarm. Used by the manager to scan /// field selects scope:
/// for stalled coordination — the per-agent equivalent on the /// - `None` — the manager's own loose ends: approvals it
/// sub-agent surface is `AgentRequest::GetLooseEnds` which /// submitted + questions where it is asker/target + its own
/// only returns rows where the agent itself is asker / target. /// pending reminders. This is the default.
GetLooseEnds, /// - `Some("*")` — hive-wide: EVERY pending approval, unanswered
/// question, and pending reminder across the swarm.
/// - `Some("<name>")` — that specific agent's loose ends.
GetLooseEnds {
#[serde(default)]
agent: Option<String>,
},
/// Count of the manager's own pending reminders. Mirror of /// Count of the manager's own pending reminders. Mirror of
/// `AgentRequest::CountPendingReminders` on the manager surface. /// `AgentRequest::CountPendingReminders` on the manager surface.
CountPendingReminders, CountPendingReminders,