feat(#2659): serve hive-matrix-mcp over persistent streamable-http, drop stdio bridge

This commit is contained in:
damocles 2026-07-23 20:20:51 +02:00 committed by mara
commit a66b7ab298
21 changed files with 411 additions and 856 deletions

View file

@ -1,8 +1,9 @@
//! Formatting / render helpers for the MCP tool surface: ack / recv /
//! loose-end / agent-meta reply shaping plus the retry annotation.
//! Stateless string builders, with one exception —
//! [`matrix_unread_summary`] queries the local matrix daemon socket
//! (best-effort) so `get_loose_ends` can prepend an unread-rooms entry.
//! [`matrix_unread_summary`] queries `hive-matrix-daemon`'s local
//! `/unread-summary` status endpoint (best-effort) so `get_loose_ends`
//! can prepend an unread-rooms entry.
/// Render the three identical failure arms every data-returning tool handler
/// repeats: a broker `Err` → `"{tool} failed: {m}"`, an unexpected `Ok` variant
@ -272,27 +273,30 @@ pub(super) struct MatrixRoomUnread {
last_sender: Option<String>,
}
/// Query the local matrix daemon for per-room unread summaries. Returns
/// `None` if the daemon socket is absent or the query fails. Best-effort:
/// agents without matrix configured are not penalised.
/// Default port `hive-matrix-daemon` serves its MCP + status endpoints
/// on (`hyperhive.mcp.matrixHttpPort`'s nix default). Overridable via
/// `HIVE_MATRIX_HTTP_PORT` for parity with the port options nix already
/// exposes; unset in practice since a single fixed port is safe (each
/// agent container is its own network namespace — see docs/network.md).
const DEFAULT_MATRIX_HTTP_PORT: u16 = 8792;
/// Short request timeout for the local status query below — this must
/// never stall a turn waiting on a wedged same-container daemon.
const MATRIX_STATUS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
/// Query the local matrix daemon's `/unread-summary` status endpoint
/// for per-room unread summaries. Returns `None` if the daemon isn't
/// reachable or the query fails. Best-effort: agents without matrix
/// configured are not penalised.
pub(super) async fn matrix_unread_summary() -> Option<Vec<MatrixRoomUnread>> {
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
let socket = std::env::var_os("HIVE_MATRIX_SOCKET").map_or_else(
|| std::path::PathBuf::from("/run/hive-matrix/socket"),
std::path::PathBuf::from,
);
if !socket.exists() {
return None;
}
let mut stream = UnixStream::connect(&socket).await.ok()?;
stream
.write_all(b"{\"method\":\"unread_summary\"}\n")
.await
let port = std::env::var("HIVE_MATRIX_HTTP_PORT")
.unwrap_or_else(|_| DEFAULT_MATRIX_HTTP_PORT.to_string());
let url = format!("http://127.0.0.1:{port}/unread-summary");
let client = reqwest::Client::builder()
.timeout(MATRIX_STATUS_TIMEOUT)
.build()
.ok()?;
let mut lines = BufReader::new(stream).lines();
let line = lines.next_line().await.ok()??;
let val: serde_json::Value = serde_json::from_str(&line).ok()?;
let val: serde_json::Value = client.get(&url).send().await.ok()?.json().await.ok()?;
// Response: {"kind":"ok","payload":[{label, count, last_body?, last_sender?}]}
let arr = val.get("payload")?.as_array()?;
serde_json::from_value(serde_json::Value::Array(arr.clone())).ok()