feat(#1137): rich unread summary in loose ends and wake signal

- hive-sh4re: UnreadMatrix gains summary: String field (per-room breakdown)
- hive-matrix-mcp/protocol: add RoomUnread struct + UnreadSummary request
- hive-matrix-mcp/handlers: collect_unread() fetches per-room data;
  single-unread rooms include truncated last-message body + sender;
  multi-unread rooms carry count only
- hive-matrix-mcp/wake: format_unread_summary() builds wake body from
  RoomUnread slice; terse one-liner for single-room/single-message,
  bulleted list for multi-room; always appends read-hint
- hive-matrix-mcp/timeline: wake body now covers all rooms with unread
  at fire time, not just the triggering event; falls back to per-event
  teaser if notification counts haven't updated yet
- hive-ag3nt/mcp: matrix_unread_summary() replaces matrix_unread_rooms();
  UnreadMatrix loose end carries per-room summary lines; render shows
  room breakdown with sender: body for single-unread rooms
This commit is contained in:
atlas 2026-06-03 12:52:24 +02:00
commit 68e30b857c
19 changed files with 421 additions and 108 deletions

View file

@ -87,6 +87,13 @@ pub enum DaemonRequest {
#[serde(rename = "unread_count")]
UnreadCount,
/// Return per-room unread summaries. For rooms with exactly one
/// unread notification, attempts to include the sender + truncated
/// body; rooms with multiple unreads carry only the count. Used by
/// `get_loose_ends` and the wake-signal formatter.
#[serde(rename = "unread_summary")]
UnreadSummary,
/// Liveness probe — fast "are you up?" round-trip that doesn't
/// touch matrix-sdk. Not used by the in-tree stdio MCP bridge
/// (which surfaces a daemon-down condition as a normal tool-call
@ -96,6 +103,23 @@ pub enum DaemonRequest {
Ping,
}
/// One entry in the [`DaemonRequest::UnreadSummary`] response payload.
#[derive(Debug, Serialize, Deserialize)]
pub struct RoomUnread {
/// Canonical alias (`#name:server`) or room id (`!id:server`).
pub label: String,
/// Server-side push-notification count for this room. Always ≥ 1.
pub count: u32,
/// Truncated body of the last message in the room. Present only when
/// `count == 1` and the fetch succeeded; absent otherwise.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_body: Option<String>,
/// Sender of the last message (`@user:server`). Present when
/// `last_body` is present.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_sender: Option<String>,
}
/// Response shape: `ok` carries the payload (any JSON; the MCP bridge
/// passes it back to claude as the tool result), `error` carries a
/// human-readable error string.