rename: open_threads → loose_ends + cancel_thread → cancel_loose_end across wire / tools / web ui

This commit is contained in:
damocles 2026-05-18 18:22:49 +02:00
parent b1d0a62cb9
commit 6e23d087d2
16 changed files with 152 additions and 139 deletions

View file

@ -393,43 +393,43 @@
}
renderStateBadge();
}
// Open-threads section: same data the get_open_threads MCP tool
// Loose-ends section: same data the get_loose_ends MCP tool
// returns. Best-effort fetch on cold load + after every turn_end
// (a turn likely answered or asked something). Silent failure
// keeps the section hidden rather than surfacing an empty banner.
let lastOpenThreadsCount = 0;
async function refreshOpenThreads() {
let lastLooseEndsCount = 0;
async function refreshLooseEnds() {
try {
const resp = await fetch('/api/open-threads');
const resp = await fetch('/api/loose-ends');
if (!resp.ok) {
renderOpenThreads([]);
renderLooseEnds([]);
return;
}
const data = await resp.json();
renderOpenThreads(data.threads || []);
renderLooseEnds(data.loose_ends || []);
} catch (err) {
console.warn('open-threads fetch failed', err);
renderOpenThreads([]);
console.warn('loose-ends fetch failed', err);
renderLooseEnds([]);
}
}
function renderOpenThreads(threads) {
const root = $('open-threads-section');
const list = $('open-threads-list');
const summary = $('open-threads-summary');
function renderLooseEnds(threads) {
const root = $('loose-ends-section');
const list = $('loose-ends-list');
const summary = $('loose-ends-summary');
if (!root || !list || !summary) return;
if (!threads.length) {
root.hidden = true;
lastOpenThreadsCount = 0;
lastLooseEndsCount = 0;
return;
}
root.hidden = false;
summary.textContent = 'open threads · ' + threads.length;
summary.textContent = 'loose ends · ' + threads.length;
list.innerHTML = '';
// Auto-expand on first appearance of any open thread so the
// operator notices new loose ends; collapse only on operator
// click (sticky after that).
if (lastOpenThreadsCount === 0) root.open = true;
lastOpenThreadsCount = threads.length;
if (lastLooseEndsCount === 0) root.open = true;
lastLooseEndsCount = threads.length;
const fmtAge = (s) => {
if (s < 60) return s + 's';
if (s < 3600) return Math.floor(s / 60) + 'm';
@ -455,6 +455,18 @@
el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.question || ''),
);
} else if (t.kind === 'reminder') {
// due_at is an absolute unix-seconds value; show time-until-fire
// (negative when overdue, fmtAge handles 0/positive case here).
const now = Math.floor(Date.now() / 1000);
const dueIn = (t.due_at || 0) - now;
const dueLabel = dueIn >= 0 ? 'in ' + fmtAge(dueIn) : fmtAge(-dueIn) + ' overdue';
li.append(
el('span', { class: 'inbox-from' }, '⏰ reminder #' + t.id), ' ',
el('span', { class: 'inbox-sep' }, t.owner + ' · due ' + dueLabel), ' ',
el('span', { class: 'inbox-ts' }, 'scheduled ' + fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.message || ''),
);
} else {
li.append(el('span', { class: 'inbox-body' }, JSON.stringify(t)));
}
@ -614,7 +626,7 @@
// Open-threads aren't part of /api/state (kept on the broker
// db, fetched via the per-agent socket). Cold-load fetches
// it here; turn_end refreshes it via the renderer below.
refreshOpenThreads();
refreshLooseEnds();
// Skip the re-render if nothing structurally changed. The most
// common case is `online` polling itself — without this guard, the
// operator's <input value> gets clobbered every cycle.
@ -956,7 +968,7 @@
} else {
setBannerActive(false); setState('idle');
// Likely answered/asked/scheduled something — refresh.
refreshOpenThreads();
refreshLooseEnds();
}
const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail';
api.row(cls,

View file

@ -29,9 +29,9 @@
<ul id="inbox-list"></ul>
</details>
<details id="open-threads-section" class="agent-inbox" hidden>
<summary><span id="open-threads-summary">open threads</span></summary>
<ul id="open-threads-list"></ul>
<details id="loose-ends-section" class="agent-inbox" hidden>
<summary><span id="loose-ends-summary">loose ends</span></summary>
<ul id="loose-ends-list"></ul>
</details>
<div class="terminal-wrap">

View file

@ -7,8 +7,8 @@ Tools (hyperhive surface):
- (some agents only) **extra MCP tools** surfaced as `mcp__<server>__<tool>` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time.
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the human operator (default, or `to: "operator"`) OR a peer agent (`to: "<agent-name>"`). Returns immediately with a question id — do NOT wait inline. When the recipient answers, a system message with event `question_answered { id, question, answer, answerer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, choice between options, or peer Q&A without burning regular inbox slots. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the answerer pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: "ttl-watchdog"`) when the decision becomes moot.
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the manager calls `ask(to: "<your-name>", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target.
- `mcp__hyperhive__get_open_threads()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology.
- `mcp__hyperhive__cancel_thread(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks) or `"reminder"` (hard-deleted before it fires). `id` from the matching `get_open_threads` row or the original submission reply.
- `mcp__hyperhive__get_loose_ends()` — list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args, cheap server-side sweep. Useful at turn start to remember what's outstanding without scanning inbox archaeology.
- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel one of your own open threads. `kind` is `"question"` (the asker — you, in this case — gets a `[cancelled by <you>]` answer so the waiter unblocks) or `"reminder"` (hard-deleted before it fires). `id` from the matching `get_loose_ends` row or the original submission reply.
- `mcp__hyperhive__whoami()` — self-introspection: returns your canonical agent name (from socket identity, not the prompt-substituted label), role, and current hyperhive rev. No args. Use it when you want a trustworthy identity stamp for state files, commit messages, or cross-agent attribution that won't drift across renames.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `manager`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config.

View file

@ -12,8 +12,8 @@ Tools (hyperhive surface):
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — submit a config change for any agent (`hm1nd` for self) for operator approval. Pass an optional `description` and it appears on the dashboard approval card so the operator knows what changed without opening the diff. At submit time hive-c0re fetches your commit into the agent's applied repo and pins it as `proposal/<id>`; from that moment your proposed-side commit can be amended or force-pushed freely without changing what the operator will build.
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the operator (default, or `to: "operator"`) OR a sub-agent (`to: "<agent-name>"`). Returns immediately with a question id; the answer arrives later as a system `question_answered { id, question, answer, answerer }` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline (capped at 6h server-side) — on expiry the answer is `[expired]` and `answerer` is `"ttl-watchdog"`. Do not poll inside the same turn — finish the current work and react when the event lands.
- `mcp__hyperhive__answer(id, answer)` — answer a question that was routed to YOU (a sub-agent did `ask(to: "manager", ...)`). The triggering event in your inbox is `question_asked { id, asker, question, options, multi }`. The answer surfaces in the asker's inbox as a `question_answered` event.
- `mcp__hyperhive__get_open_threads()` — hive-wide loose ends: every pending approval + every unanswered question + every pending reminder across the swarm. Cheap server-side sweep, no args. Use to find stalled threads (sub-agent A asked B something three days ago and B never answered) before they rot.
- `mcp__hyperhive__cancel_thread(kind, id)` — cancel any question or reminder in the swarm (manager bypasses the owner check used on sub-agents). Use for hive-wide cleanup when a sub-agent is offline / can't withdraw its own ask / reminder.
- `mcp__hyperhive__get_loose_ends()` — hive-wide loose ends: every pending approval + every unanswered question + every pending reminder across the swarm. Cheap server-side sweep, no args. Use to find stalled threads (sub-agent A asked B something three days ago and B never answered) before they rot.
- `mcp__hyperhive__cancel_loose_end(kind, id)` — cancel any question or reminder in the swarm (manager bypasses the owner check used on sub-agents). Use for hive-wide cleanup when a sub-agent is offline / can't withdraw its own ask / reminder.
- `mcp__hyperhive__whoami()` — self-introspection: canonical name (`manager`), role, current hyperhive rev. No args. Useful for boot announcements and cross-agent attribution that won't drift across config reloads.
Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day.

View file

@ -240,7 +240,7 @@ async fn serve(
| AgentResponse::Status { .. }
| AgentResponse::Recent { .. }
| AgentResponse::QuestionQueued { .. }
| AgentResponse::OpenThreads { .. }
| AgentResponse::LooseEnds { .. }
| AgentResponse::PendingRemindersCount { .. }
| AgentResponse::Whoami { .. },
) => {
@ -320,11 +320,11 @@ fn now_unix() -> i64 {
async fn fetch_agent_post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
let threads = match client::request::<_, AgentResponse>(
socket,
&AgentRequest::GetOpenThreads,
&AgentRequest::GetLooseEnds,
)
.await
{
Ok(AgentResponse::OpenThreads { threads }) => u64::try_from(threads.len()).ok(),
Ok(AgentResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
_ => None,
};
let reminders = match client::request::<_, AgentResponse>(

View file

@ -208,7 +208,7 @@ async fn serve(
| ManagerResponse::QuestionQueued { .. }
| ManagerResponse::Recent { .. }
| ManagerResponse::Logs { .. }
| ManagerResponse::OpenThreads { .. }
| ManagerResponse::LooseEnds { .. }
| ManagerResponse::PendingRemindersCount { .. }
| ManagerResponse::Whoami { .. },
) => {
@ -259,11 +259,11 @@ fn now_unix() -> i64 {
async fn fetch_manager_post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
let threads = match client::request::<_, ManagerResponse>(
socket,
&ManagerRequest::GetOpenThreads,
&ManagerRequest::GetLooseEnds,
)
.await
{
Ok(ManagerResponse::OpenThreads { threads }) => u64::try_from(threads.len()).ok(),
Ok(ManagerResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
_ => None,
};
let reminders = match client::request::<_, ManagerResponse>(

View file

@ -40,7 +40,7 @@ pub enum SocketReply {
QuestionQueued(i64),
Recent(Vec<hive_sh4re::InboxRow>),
Logs(String),
OpenThreads(Vec<hive_sh4re::OpenThread>),
LooseEnds(Vec<hive_sh4re::LooseEnd>),
PendingRemindersCount(u64),
Whoami {
name: String,
@ -59,7 +59,7 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
hive_sh4re::AgentResponse::Status { unread } => Self::Status(unread),
hive_sh4re::AgentResponse::Recent { rows } => Self::Recent(rows),
hive_sh4re::AgentResponse::QuestionQueued { id } => Self::QuestionQueued(id),
hive_sh4re::AgentResponse::OpenThreads { threads } => Self::OpenThreads(threads),
hive_sh4re::AgentResponse::LooseEnds { loose_ends } => Self::LooseEnds(loose_ends),
hive_sh4re::AgentResponse::PendingRemindersCount { count } => {
Self::PendingRemindersCount(count)
}
@ -87,7 +87,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
hive_sh4re::ManagerResponse::Recent { rows } => Self::Recent(rows),
hive_sh4re::ManagerResponse::Logs { content } => Self::Logs(content),
hive_sh4re::ManagerResponse::OpenThreads { threads } => Self::OpenThreads(threads),
hive_sh4re::ManagerResponse::LooseEnds { loose_ends } => Self::LooseEnds(loose_ends),
hive_sh4re::ManagerResponse::PendingRemindersCount { count } => {
Self::PendingRemindersCount(count)
}
@ -128,16 +128,16 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
}
}
/// Format helper for `get_open_threads`: renders a short bulleted list
/// Format helper for `get_loose_ends`: renders a short bulleted list
/// of pending approvals + questions. Empty list collapses to a clear
/// marker so claude doesn't go hunting for a payload that isn't there.
pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
pub fn format_loose_ends(resp: Result<SocketReply, anyhow::Error>) -> String {
use std::fmt::Write as _;
let threads = match resp {
Ok(SocketReply::OpenThreads(t)) => t,
Ok(SocketReply::Err(m)) => return format!("get_open_threads failed: {m}"),
Ok(other) => return format!("get_open_threads unexpected response: {other:?}"),
Err(e) => return format!("get_open_threads transport error: {e:#}"),
Ok(SocketReply::LooseEnds(t)) => t,
Ok(SocketReply::Err(m)) => return format!("get_loose_ends failed: {m}"),
Ok(other) => return format!("get_loose_ends unexpected response: {other:?}"),
Err(e) => return format!("get_loose_ends transport error: {e:#}"),
};
if threads.is_empty() {
return "(no open threads)".to_owned();
@ -145,7 +145,7 @@ pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
let mut out = format!("{} open thread(s):\n", threads.len());
for t in &threads {
match t {
hive_sh4re::OpenThread::Approval {
hive_sh4re::LooseEnd::Approval {
id,
agent,
commit_ref,
@ -161,7 +161,7 @@ pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
"- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}"
);
}
hive_sh4re::OpenThread::Question {
hive_sh4re::LooseEnd::Question {
id,
asker,
target,
@ -174,7 +174,7 @@ pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
"- question #{id} ({asker} → {to}, {age_seconds}s old): {question}"
);
}
hive_sh4re::OpenThread::Reminder {
hive_sh4re::LooseEnd::Reminder {
id,
owner,
message,
@ -191,16 +191,16 @@ pub fn format_open_threads(resp: Result<SocketReply, anyhow::Error>) -> String {
out
}
/// Parse the user-facing `kind` string for `cancel_thread` into the
/// Parse the user-facing `kind` string for `cancel_loose_end` into the
/// wire enum. Accepts a small alias set so claude doesn't have to
/// remember the exact spelling (`"q"` / `"r"` shorthand falls out
/// for free).
fn parse_cancel_kind(raw: &str) -> Result<hive_sh4re::CancelThreadKind, String> {
fn parse_loose_end_kind(raw: &str) -> Result<hive_sh4re::CancelLooseEndKind, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"question" | "q" => Ok(hive_sh4re::CancelThreadKind::Question),
"reminder" | "r" => Ok(hive_sh4re::CancelThreadKind::Reminder),
"question" | "q" => Ok(hive_sh4re::CancelLooseEndKind::Question),
"reminder" | "r" => Ok(hive_sh4re::CancelLooseEndKind::Reminder),
other => Err(format!(
"cancel_thread: unknown kind '{other}' (expected \"question\" or \"reminder\")"
"cancel_loose_end: unknown kind '{other}' (expected \"question\" or \"reminder\")"
)),
}
}
@ -454,13 +454,13 @@ impl AgentServer {
sweep, no args. Useful at turn start to remember what you owe / what's owed to \
you without scrolling inbox history. Output is a short bulleted list with ids, \
ages in seconds, and the relevant context. Each `question` or `reminder` row \
can be cancelled by passing its id + kind to `cancel_thread`. Empty result \
can be cancelled by passing its id + kind to `cancel_loose_end`. Empty result \
is reported clearly."
)]
async fn get_open_threads(&self) -> String {
run_tool_envelope("get_open_threads", String::new(), async move {
let (resp, retries) = self.dispatch(hive_sh4re::AgentRequest::GetOpenThreads).await;
annotate_retries(format_open_threads(resp), retries)
async fn get_loose_ends(&self) -> String {
run_tool_envelope("get_loose_ends", String::new(), async move {
let (resp, retries) = self.dispatch(hive_sh4re::AgentRequest::GetLooseEnds).await;
annotate_retries(format_loose_ends(resp), retries)
})
.await
}
@ -485,24 +485,24 @@ impl AgentServer {
description = "Cancel an open thread you own — a `question` you asked (the \
asker gets `[cancelled by <you>]` as the answer and unblocks) or a `reminder` \
you scheduled (hard-deleted before it fires). `kind` is `\"question\"` or \
`\"reminder\"`; `id` is the row id from the matching `get_open_threads` entry \
`\"reminder\"`; `id` is the row id from the matching `get_loose_ends` entry \
or the `question_queued` reply you got when you submitted. Auth: you can only \
cancel rows where you're the asker / owner. Returns `ok` or an error string."
)]
async fn cancel_thread(&self, Parameters(args): Parameters<CancelThreadArgs>) -> String {
async fn cancel_loose_end(&self, Parameters(args): Parameters<CancelLooseEndArgs>) -> String {
let log = format!("{args:?}");
let kind_label = args.kind.clone();
let id = args.id;
run_tool_envelope("cancel_thread", log, async move {
let kind = match parse_cancel_kind(&args.kind) {
run_tool_envelope("cancel_loose_end", log, async move {
let kind = match parse_loose_end_kind(&args.kind) {
Ok(k) => k,
Err(e) => return e,
};
let (resp, retries) = self
.dispatch(hive_sh4re::AgentRequest::CancelThread { kind, id })
.dispatch(hive_sh4re::AgentRequest::CancelLooseEnd { kind, id })
.await;
annotate_retries(
format_ack(resp, "cancel_thread", format!("cancelled {kind_label} {id}")),
format_ack(resp, "cancel_loose_end", format!("cancelled {kind_label} {id}")),
retries,
)
})
@ -654,13 +654,13 @@ pub struct AnswerArgs {
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CancelThreadArgs {
pub struct CancelLooseEndArgs {
/// Which kind of thread to cancel — `"question"` for an open
/// `ask` that's still waiting on an answer, `"reminder"` for a
/// scheduled `remind` that hasn't fired yet. Use the `kind`
/// field straight off the `get_open_threads` row.
/// field straight off the `get_loose_ends` row.
pub kind: String,
/// Row id from the matching `get_open_threads` entry (or the
/// Row id from the matching `get_loose_ends` entry (or the
/// `question_queued` reply when you submitted it).
pub id: i64,
}
@ -995,14 +995,14 @@ impl ManagerServer {
approvals stuck waiting on the operator, reminders piling up on an offline \
agent, etc. No args. The sub-agent flavour only returns the agent's own \
threads; the manager flavour is unfiltered. Cancel any question or reminder \
row via `cancel_thread` (manager bypasses the owner check)."
row via `cancel_loose_end` (manager bypasses the owner check)."
)]
async fn get_open_threads(&self) -> String {
run_tool_envelope("get_open_threads", String::new(), async move {
async fn get_loose_ends(&self) -> String {
run_tool_envelope("get_loose_ends", String::new(), async move {
let (resp, retries) = self
.dispatch(hive_sh4re::ManagerRequest::GetOpenThreads)
.dispatch(hive_sh4re::ManagerRequest::GetLooseEnds)
.await;
annotate_retries(format_open_threads(resp), retries)
annotate_retries(format_loose_ends(resp), retries)
})
.await
}
@ -1025,24 +1025,24 @@ impl ManagerServer {
description = "Cancel any open thread in the swarm — a `question` (cancels \
with the operator-override sentinel so the asker unblocks) or a `reminder` \
(hard-deleted before fire). `kind` is `\"question\"` or `\"reminder\"`; `id` \
is the row id from `get_open_threads` or the original submission reply. \
is the row id from `get_loose_ends` or the original submission reply. \
Manager surface bypasses the owner check on the sub-agent flavour use for \
hive-wide cleanup of stuck or stale threads."
)]
async fn cancel_thread(&self, Parameters(args): Parameters<CancelThreadArgs>) -> String {
async fn cancel_loose_end(&self, Parameters(args): Parameters<CancelLooseEndArgs>) -> String {
let log = format!("{args:?}");
let kind_label = args.kind.clone();
let id = args.id;
run_tool_envelope("cancel_thread", log, async move {
let kind = match parse_cancel_kind(&args.kind) {
run_tool_envelope("cancel_loose_end", log, async move {
let kind = match parse_loose_end_kind(&args.kind) {
Ok(k) => k,
Err(e) => return e,
};
let (resp, retries) = self
.dispatch(hive_sh4re::ManagerRequest::CancelThread { kind, id })
.dispatch(hive_sh4re::ManagerRequest::CancelLooseEnd { kind, id })
.await;
annotate_retries(
format_ack(resp, "cancel_thread", format!("cancelled {kind_label} {id}")),
format_ack(resp, "cancel_loose_end", format!("cancelled {kind_label} {id}")),
retries,
)
})
@ -1092,8 +1092,8 @@ impl ManagerServer {
any agent including yourself), `ask` (structured question to the operator or a \
sub-agent non-blocking, answer arrives later as a `question_answered` event), \
`answer` (respond to a `question_asked` event directed at you), \
`get_open_threads` (hive-wide loose ends pending approvals + unanswered \
questions + pending reminders across the swarm), `cancel_thread` (cancel any \
`get_loose_ends` (hive-wide loose ends pending approvals + unanswered \
questions + pending reminders across the swarm), `cancel_loose_end` (cancel any \
question or reminder row by id), `whoami` (self-introspection canonical \
name, role, current hyperhive rev). The manager's own config lives at \
`/agents/hm1nd/config/agent.nix`."
@ -1136,9 +1136,9 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
"ask",
"answer",
"remind",
"get_open_threads",
"get_loose_ends",
"whoami",
"cancel_thread",
"cancel_loose_end",
],
Flavor::Manager => &[
"send",
@ -1152,10 +1152,10 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
"ask",
"answer",
"get_logs",
"get_open_threads",
"get_loose_ends",
"remind",
"whoami",
"cancel_thread",
"cancel_loose_end",
],
};
let mut out: Vec<String> = names

View file

@ -106,7 +106,7 @@ pub async fn serve(
.route("/api/compact", post(post_compact))
.route("/api/model", post(post_set_model))
.route("/api/new-session", post(post_new_session))
.route("/api/open-threads", get(api_open_threads))
.route("/api/loose-ends", get(api_loose_ends))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = bind_with_retry(addr, "web UI").await?;
@ -240,25 +240,25 @@ struct SessionView {
exit_note: Option<String>,
}
/// Proxy this agent's open-threads via the per-agent socket. The
/// Proxy this agent's loose-ends list via the per-agent socket. The
/// web UI surfaces the result as a collapsible section in the page
/// so the operator can see at a glance what's pending against the
/// agent (questions asked by it, peer questions targeting it,
/// approvals for the manager). Same data the
/// `mcp__hyperhive__get_open_threads` tool sees from inside the
/// reminders it scheduled, approvals for the manager). Same data
/// the `mcp__hyperhive__get_loose_ends` tool sees from inside the
/// container.
async fn api_open_threads(State(state): State<AppState>) -> Response {
let threads: Vec<hive_sh4re::OpenThread> = match state.flavor() {
async fn api_loose_ends(State(state): State<AppState>) -> Response {
let loose_ends: Vec<hive_sh4re::LooseEnd> = match state.flavor() {
Flavor::Agent => {
match client::request::<_, hive_sh4re::AgentResponse>(
&state.socket,
&hive_sh4re::AgentRequest::GetOpenThreads,
&hive_sh4re::AgentRequest::GetLooseEnds,
)
.await
{
Ok(hive_sh4re::AgentResponse::OpenThreads { threads }) => threads,
Ok(hive_sh4re::AgentResponse::LooseEnds { loose_ends }) => loose_ends,
Ok(hive_sh4re::AgentResponse::Err { message }) => {
return error_response(&format!("get_open_threads: {message}"));
return error_response(&format!("get_loose_ends: {message}"));
}
Ok(other) => return error_response(&format!("unexpected response: {other:?}")),
Err(e) => return error_response(&format!("transport: {e:#}")),
@ -267,20 +267,20 @@ async fn api_open_threads(State(state): State<AppState>) -> Response {
Flavor::Manager => {
match client::request::<_, hive_sh4re::ManagerResponse>(
&state.socket,
&hive_sh4re::ManagerRequest::GetOpenThreads,
&hive_sh4re::ManagerRequest::GetLooseEnds,
)
.await
{
Ok(hive_sh4re::ManagerResponse::OpenThreads { threads }) => threads,
Ok(hive_sh4re::ManagerResponse::LooseEnds { loose_ends }) => loose_ends,
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
return error_response(&format!("get_open_threads: {message}"));
return error_response(&format!("get_loose_ends: {message}"));
}
Ok(other) => return error_response(&format!("unexpected response: {other:?}")),
Err(e) => return error_response(&format!("transport: {e:#}")),
}
}
};
axum::Json(serde_json::json!({ "threads": threads })).into_response()
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
}
async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {