//! Matrix event handlers: incoming room messages fire a hyperhive //! wake signal so the agent's harness drives a new claude turn. //! //! Per mara on #548: wake body is a SHORT TEASER, not the full message //! (msg stays unread server-side; agent fetches via `read_room`). The //! `wake::format_wake_body` truncates to ~100 chars. //! //! Self-events (events sent by this agent) are filtered out so an //! agent posting a message doesn't wake itself. use std::path::PathBuf; use std::sync::Arc; use matrix_sdk::{ Client, Room, RoomState, ruma::events::room::member::{MembershipState, StrippedRoomMemberEvent}, ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, }; use crate::{handlers, wake}; /// Install the room-message handler on `client`. Fires on every /// `m.room.message` event in a joined room; non-self messages trigger /// a wake signal to the hyperhive harness via the unix socket at /// `hyperhive_socket`. The wake body summarises ALL rooms with unread /// notifications at wake time (not just the triggering event) so the /// agent receives a full picture in one prompt. pub fn install_message_handler(client: &Client, hyperhive_socket: PathBuf) { let socket = Arc::new(hyperhive_socket); let own_user = client.user_id().map(std::borrow::ToOwned::to_owned); client.add_event_handler({ let socket = socket.clone(); move |event: OriginalSyncRoomMessageEvent, room: Room, client: Client| { let socket = socket.clone(); let own_user = own_user.clone(); async move { if room.state() != RoomState::Joined { return; } // Self-event filter so the agent's own outgoing messages // don't wake it. matches forge_notify's self-skip pattern. if own_user.as_ref().is_some_and(|u| u == &event.sender) { return; } // Build a wake body that covers all rooms with unread // notifications, not just the triggering event. This lets // the agent see the full backlog in a single wake prompt. // Falls back to the per-event teaser if the collect fails // (empty result means daemon is not seeing any unread yet — // unlikely but possible during a sync race). let unread = handlers::collect_unread(&client).await; let body = if unread.is_empty() { // Sync hasn't updated notification counts yet; fall back // to the current event so the agent still wakes. let text = match &event.content.msgtype { MessageType::Text(t) => t.body.clone(), MessageType::Notice(n) => n.body.clone(), MessageType::Emote(e) => format!("* {}", e.body), _ => format!("[{}]", event.content.msgtype()), }; let room_label = room .canonical_alias() .map_or_else(|| room.room_id().to_string(), |a| a.to_string()); wake::format_wake_body(event.sender.as_str(), &room_label, &text) } else { wake::format_unread_summary(&unread) }; if let Err(e) = wake::send_wake(&socket, &body).await { tracing::warn!(error = %e, "failed to deliver matrix wake to hyperhive"); } else { tracing::debug!(room = %room.room_id(), "matrix wake delivered"); } } } }); tracing::info!("matrix message handler installed"); } /// Install a handler that auto-accepts room invites as soon as they /// arrive during sync. When the daemon receives an `m.room.member` /// state event with `membership: invite` for the agent's own user ID, /// it calls `join_room_by_id` immediately and fires a hyperhive wake /// so the agent learns about the new room on its next turn. /// /// This closes the gap where agents were provisioned and invited to the /// hive Space by hive-c0re but never received the invite notification /// because the handler wasn't registered — the invite sat in the /// `invited_rooms()` list forever until the agent called `join_room` /// manually. pub fn install_invite_handler(client: &Client, hyperhive_socket: PathBuf) { let socket = Arc::new(hyperhive_socket); let own_user = client.user_id().map(std::borrow::ToOwned::to_owned); client.add_event_handler({ move |event: StrippedRoomMemberEvent, room: Room, client: Client| { let socket = socket.clone(); let own_user = own_user.clone(); async move { // Only handle invite events for our own user. if event.content.membership != MembershipState::Invite { return; } let is_ours = own_user .as_ref() .is_some_and(|u| u.as_str() == event.state_key.as_str()); if !is_ours { return; } let room_id = room.room_id().to_owned(); match client.join_room_by_id(&room_id).await { Ok(_) => { tracing::info!(%room_id, "matrix: auto-accepted invite"); let label = room.name().unwrap_or_else(|| room_id.to_string()); let body = format!( "[matrix] invited to {label} — auto-accepted; \ use read_room to view messages" ); if let Err(e) = wake::send_wake(&socket, &body).await { tracing::warn!( error = %e, "matrix: failed to deliver invite-wake to hyperhive" ); } } Err(e) => { tracing::warn!(error = %e, %room_id, "matrix: auto-accept invite failed"); } } } } }); tracing::info!("matrix invite handler installed"); }