feat(#2569): migrate matrix producer to the in-agent todo socket (unread + invites); drop dead mcp.sock/wake plumbing

This commit is contained in:
damocles 2026-07-20 23:09:54 +02:00
commit 21f1569a04
5 changed files with 113 additions and 166 deletions

View file

@ -1,17 +1,14 @@
//! Wake-signal writer: notifies the hyperhive harness when an incoming
//! matrix event arrives so claude drives a new turn.
//! Todo writer: pushes matrix *todos* (loose-ends v2) to the harness's
//! in-agent socket (`HIVE_AGENT_SOCKET`) when rooms have unread messages
//! or pending invites, so claude drives a turn to handle them. One JSON
//! line per op (`upsert_todo` / `clear_todo`), keyed by room id so
//! re-pushing an unchanged item is an idempotent no-op and resolving one
//! clears it. The harness owns the todo store locally and signals its own
//! turn loop — no hive-c0re round-trip.
//!
//! Same wire shape as `hive-agent::forge_notify`'s wake: a single JSON
//! line written to the hyperhive control socket (`/run/hive/mcp.sock`
//! by default) carrying an `Request::Wake { from, body }`.
//! The agent harness's `agent_server` parses it and treats it as a
//! `Wake` from the matrix subsystem.
//!
//! Per the operator's call (phase 3): the body is a SHORT TEASER, not
//! the full message — the agent then reads the unmarked event via
//! the `read_room` MCP tool. Truncation to ~100 chars keeps the wake
//! prompt focused (`forge_notify` embeds longer excerpts because the
//! agent doesn't have a follow-up read-the-original tool for forge).
//! Todo summaries stay short: a SHORT TEASER, not the full message — the
//! agent then reads the unmarked event via the `read_room` MCP tool.
//! Truncation to ~100 chars keeps the summary focused.
use std::path::Path;
@ -19,65 +16,59 @@ use anyhow::{Context, Result};
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
/// Send an `Request::Wake { from: "matrix", body }` to the hyperhive
/// control socket at `socket`. Best-effort: returns Err on any plumbing
/// failure; callers log + ignore so a wake delivery hiccup doesn't tear
/// down the matrix sync loop.
///
/// Wire format matches `hive_core_agent_sock::Request` tagged with `"cmd"` per
/// `#[serde(tag = "cmd", rename_all = "snake_case")]`. Must be `"cmd"`,
/// not `"kind"` — the harness deserialises against the hive-sh4re type
/// and silently discards requests that don't match.
///
/// # Errors
///
/// Returns an error on socket connect failure, serialisation failure,
/// or I/O error writing to or reading from the socket.
pub async fn send_wake(socket: &Path, body: impl AsRef<str>) -> Result<()> {
let payload = serde_json::json!({
"cmd": "wake",
"from": "matrix",
"body": body.as_ref(),
"transient": true,
});
send_line(socket, &payload).await
/// The harness-served in-agent socket (`HIVE_AGENT_SOCKET`) where todo ops
/// go — distinct from the host-served control socket used by [`send_wake`].
/// `None` when unset/empty, in which case todo sends are a best-effort
/// no-op (a standalone daemon without the harness socket).
fn agent_socket() -> Option<std::path::PathBuf> {
std::env::var_os("HIVE_AGENT_SOCKET")
.filter(|v| !v.is_empty())
.map(std::path::PathBuf::from)
}
/// Upsert a matrix-subsystem *todo* (loose-ends v2) on the
/// hyperhive control socket — the replacement for a direct wake. `key` is
/// the room id (the dedup key); hive-c0re coalesces a wake iff the todo is
/// new or its `summary` changed. Best-effort like [`send_wake`].
/// Upsert a matrix-subsystem *todo* (loose-ends v2) on the harness's
/// in-agent socket — the replacement for a direct wake. `key` is the room
/// id (the dedup key); the harness signals a turn iff the todo is new or
/// its `summary` changed. Best-effort: a no-op when `HIVE_AGENT_SOCKET`
/// isn't configured.
///
/// # Errors
///
/// Returns an error on socket connect failure, serialisation failure,
/// or I/O error writing to or reading from the socket.
pub async fn send_todo_upsert(socket: &Path, key: &str, summary: impl AsRef<str>) -> Result<()> {
pub async fn send_todo_upsert(key: &str, summary: impl AsRef<str>) -> Result<()> {
let Some(socket) = agent_socket() else {
return Ok(());
};
let payload = serde_json::json!({
"cmd": "upsert_todo",
"subsystem": "matrix",
"key": key,
"summary": summary.as_ref(),
});
send_line(socket, &payload).await
send_line(&socket, &payload).await
}
/// Clear matrix-subsystem todos. `key = Some(room)` clears one room's
/// todo (it was read); `all = true` wipes the whole matrix set
/// (cancel-and-recreate on daemon restart). Best-effort.
/// Clear matrix-subsystem todos on the harness's in-agent socket. `key =
/// Some(room)` clears one room's todo (it was read); `all = true` wipes the
/// whole matrix set (cancel-and-recreate on daemon restart). Best-effort:
/// a no-op when `HIVE_AGENT_SOCKET` isn't configured.
///
/// # Errors
///
/// Returns an error on socket connect failure, serialisation failure,
/// or I/O error writing to or reading from the socket.
pub async fn send_todo_clear(socket: &Path, key: Option<&str>, all: bool) -> Result<()> {
pub async fn send_todo_clear(key: Option<&str>, all: bool) -> Result<()> {
let Some(socket) = agent_socket() else {
return Ok(());
};
let payload = serde_json::json!({
"cmd": "clear_todo",
"subsystem": "matrix",
"key": key,
"all": all,
});
send_line(socket, &payload).await
send_line(&socket, &payload).await
}
/// Write one JSON request line to the hyperhive control socket and drain