fix(#2639): cancel_loose_end kind="todo" clears without a bash round-trip

Adds a real MCP-side path for the workaround #2639 documented: dial the
in-container HIVE_AGENT_SOCKET directly from cancel_loose_end (kind:"todo")
instead of shelling out via a tracked bash task (nc -U ...), which is what
was spawning a fresh completion todo on every clear and cascading forever.

hive-agent-mcp/src/mcp/render.rs: renamed local_todos's socket-dial guts
into a shared dial_agent_socket(req) helper, added mark_local_todo_done(id)
on top of it (MarkTodoDone request already existed server-side, unused
until now). mod.rs wires kind:"todo" into cancel_loose_end ahead of the
question/reminder/approval parse. args.rs + render.rs + docs/tools/bash.md
text updated to point at the new path instead of the old raw nc invocation.
This commit is contained in:
damocles 2026-07-22 21:46:16 +02:00
commit c4deca99db
4 changed files with 70 additions and 20 deletions

View file

@ -35,8 +35,8 @@ pub use args::{
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
use render::{
format_matrix_summary, local_todos, loose_end_kind_label, matrix_unread_summary,
parse_loose_end_kind, render_loose_ends, reply_err,
format_matrix_summary, local_todos, loose_end_kind_label, mark_local_todo_done,
matrix_unread_summary, parse_loose_end_kind, render_loose_ends, reply_err,
};
/// Write (or remove) the status file in the agent's own `state/` directory.
@ -413,12 +413,34 @@ impl AgentServer {
cancel rows where you're the asker / owner. Returns `ok` or an error string.\n\
`kind` may also be `\"approval\"` to withdraw a pending approval you submitted \
(before the operator acts on it) root agent (`ruth`) only; the server rejects \
`approval` kind for all other callers."
`approval` kind for all other callers.\n\
`kind` may also be `\"todo\"` to clear one of your own loose-ends-v2 todos \
(bash-task completions, matrix unread, forge activity the id in the \
`get_loose_ends` `todo #N` line) this dials the in-container socket directly, \
no bash task involved, so it's safe to call repeatedly without spawning more \
todos."
)]
async fn cancel_loose_end(&self, Parameters(args): Parameters<CancelLooseEndArgs>) -> String {
let log = format!("{args:?}");
let id = args.id;
run_tool_envelope("cancel_loose_end", log, async move {
if args.kind.trim().eq_ignore_ascii_case("todo") {
return match mark_local_todo_done(id).await {
Some(hive_agent_sock::Response::Acked { count }) if count > 0 => {
format!("cleared todo {id}")
}
Some(hive_agent_sock::Response::Acked { .. }) => {
format!("no such todo {id} (already cleared, or never existed)")
}
Some(hive_agent_sock::Response::Err { message }) => {
format!("cancel_loose_end failed: {message}")
}
Some(other) => format!("cancel_loose_end unexpected response: {other:?}"),
None => "cancel_loose_end: local todo socket unavailable \
(HIVE_AGENT_SOCKET unset or harness unreachable)"
.to_owned(),
};
}
let kind = match parse_loose_end_kind(&args.kind) {
Ok(k) => k,
Err(e) => return e,