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:
parent
4989bcdb5e
commit
c4deca99db
4 changed files with 70 additions and 20 deletions
|
|
@ -252,7 +252,8 @@ pub(super) fn render_loose_ends(loose_ends: &[hive_sh4re::LooseEnd]) -> String {
|
|||
.unwrap_or_default();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"- todo #{id} [{subsystem}{key}, {age_seconds}s old]: {summary}{src} (mark_todo_done to clear)"
|
||||
"- todo #{id} [{subsystem}{key}, {age_seconds}s old]: {summary}{src} \
|
||||
(cancel_loose_end kind:\"todo\" id:{id} to clear)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -297,11 +298,17 @@ pub(super) async fn matrix_unread_summary() -> Option<Vec<MatrixRoomUnread>> {
|
|||
serde_json::from_value(serde_json::Value::Array(arr.clone())).ok()
|
||||
}
|
||||
|
||||
/// Query the harness's in-agent socket for this agent's local todos
|
||||
/// (loose-ends v2). Returns `None` when `HIVE_AGENT_SOCKET` is unset /
|
||||
/// absent or the query fails — best-effort, like [`matrix_unread_summary`],
|
||||
/// so an agent without the socket is not penalised.
|
||||
pub(super) async fn local_todos() -> Option<Vec<hive_sh4re::LooseEnd>> {
|
||||
/// Dial the harness's in-agent socket (`HIVE_AGENT_SOCKET`) with a single
|
||||
/// one-shot JSON-line request and decode the response. Returns `None` when
|
||||
/// the socket is unset/absent or on any transport/decode failure —
|
||||
/// best-effort, single-shot (no retry, unlike the broker's
|
||||
/// `client::request_retried`): this socket lives in the SAME container, so
|
||||
/// a connect failure means the harness itself isn't up, which a retry
|
||||
/// won't fix within a tool call's budget. Shared by [`local_todos`] and
|
||||
/// [`mark_local_todo_done`].
|
||||
pub(super) async fn dial_agent_socket(
|
||||
req: &hive_agent_sock::Request,
|
||||
) -> Option<hive_agent_sock::Response> {
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
let socket = std::env::var_os("HIVE_AGENT_SOCKET").map(std::path::PathBuf::from)?;
|
||||
|
|
@ -309,18 +316,36 @@ pub(super) async fn local_todos() -> Option<Vec<hive_sh4re::LooseEnd>> {
|
|||
return None;
|
||||
}
|
||||
let mut stream = UnixStream::connect(&socket).await.ok()?;
|
||||
let mut req =
|
||||
serde_json::to_string(&hive_agent_sock::Request::ListTodos { subsystem: None }).ok()?;
|
||||
req.push('\n');
|
||||
stream.write_all(req.as_bytes()).await.ok()?;
|
||||
let mut line = serde_json::to_string(req).ok()?;
|
||||
line.push('\n');
|
||||
stream.write_all(line.as_bytes()).await.ok()?;
|
||||
let mut lines = BufReader::new(stream).lines();
|
||||
let line = lines.next_line().await.ok()??;
|
||||
match serde_json::from_str::<hive_agent_sock::Response>(&line).ok()? {
|
||||
let resp_line = lines.next_line().await.ok()??;
|
||||
serde_json::from_str(&resp_line).ok()
|
||||
}
|
||||
|
||||
/// Query the harness's in-agent socket for this agent's local todos
|
||||
/// (loose-ends v2). Returns `None` when `HIVE_AGENT_SOCKET` is unset /
|
||||
/// absent or the query fails — best-effort, like [`matrix_unread_summary`],
|
||||
/// so an agent without the socket is not penalised.
|
||||
pub(super) async fn local_todos() -> Option<Vec<hive_sh4re::LooseEnd>> {
|
||||
match dial_agent_socket(&hive_agent_sock::Request::ListTodos { subsystem: None }).await? {
|
||||
hive_agent_sock::Response::LooseEnds { loose_ends } => Some(loose_ends),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark one of this agent's local todos (loose-ends v2) done by id, via
|
||||
/// the harness's in-agent socket — reachable through `cancel_loose_end`
|
||||
/// kind `"todo"` so clearing a todo never has to shell out through a
|
||||
/// tracked bash task (which would just spawn a fresh completion todo of
|
||||
/// its own). `None` means the socket is unset/absent or unreachable,
|
||||
/// distinct from a decoded `Err` response (unknown id) so the caller can
|
||||
/// tell the two apart.
|
||||
pub(super) async fn mark_local_todo_done(id: i64) -> Option<hive_agent_sock::Response> {
|
||||
dial_agent_socket(&hive_agent_sock::Request::MarkTodoDone { id }).await
|
||||
}
|
||||
|
||||
/// Format a `Vec<MatrixRoomUnread>` into a per-room summary string.
|
||||
/// Single room / single message collapses to one line; multi-room
|
||||
/// expands to a bulleted list. Returns an empty string for empty input.
|
||||
|
|
@ -357,7 +382,7 @@ pub(super) fn parse_loose_end_kind(raw: &str) -> Result<hive_sh4re::CancelLooseE
|
|||
"approval" | "a" => Ok(hive_sh4re::CancelLooseEndKind::Approval),
|
||||
other => Err(format!(
|
||||
"cancel_loose_end: unknown kind '{other}' \
|
||||
(expected \"question\", \"reminder\", or \"approval\")"
|
||||
(expected \"question\", \"reminder\", \"approval\", or \"todo\")"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue