diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index d63f720c65..3ea7900804 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -22,7 +22,7 @@ use acp::{AcpClient, EnvVar, McpServer}; use anyhow::Result; use buzz_core::kind::{ KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_STREAM_MESSAGE, - KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, }; use buzz_core::observer::{ decrypt_observer_payload, encrypt_observer_payload, OBSERVER_FRAME_TELEMETRY, @@ -1440,8 +1440,12 @@ async fn tokio_main() -> Result<()> { name: "mentions".into(), channels: filter::ChannelScope::All("all".into()), kinds: config.kinds_override.clone().unwrap_or_else(|| { + // Include kind:40003 message edits so adding an @mention + // via edit can wake the agent the same way a fresh send does + // (#2540). require_mention still gates on a matching `p` tag. vec![ KIND_STREAM_MESSAGE, + KIND_STREAM_MESSAGE_EDIT, KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER, ] diff --git a/desktop/src-tauri/src/events.rs b/desktop/src-tauri/src/events.rs index 777d56d02e..42432e6cad 100644 --- a/desktop/src-tauri/src/events.rs +++ b/desktop/src-tauri/src/events.rs @@ -404,10 +404,10 @@ pub fn build_forum_comment( /// /// `mentions` carries the pubkeys of mentions that are *newly added* by this /// edit (the caller diffs the edited body against the original). Only those get -/// a `p` tag so the newly-mentioned party is notified/woken, while a typo-fix -/// edit that leaves the mention set unchanged emits no `p` tags and never -/// re-wakes anyone. This mirrors the send path's `mention_tags` (dedup + -/// lowercase); the receiver overlays these onto the original event's audience. +/// a `p` tag (wake/notify) **and** a matching `mention` reference tag (explicit +/// @mention signal — same as the send path's `MENTION_REFERENCE_TAG`), while a +/// typo-fix edit that leaves the mention set unchanged emits neither and never +/// re-wakes anyone. The receiver overlays these onto the original event's audience. pub fn build_message_edit( channel_id: Uuid, target_event_id: EventId, @@ -421,7 +421,19 @@ pub fn build_message_edit( tag(vec!["h", &channel_id.to_string()])?, tag(vec!["e", &target_event_id.to_hex()])?, ]; - tags.extend(mention_tags(mentions)?); + let p_tags = mention_tags(mentions)?; + tags.extend(p_tags.clone()); + // Parity with send: explicit `mention` refs so relay offline-notice (#1743) + // and desktop resolve-mention can treat edit-added @mentions like send. + let mention_refs: Vec> = p_tags + .iter() + .filter_map(|t| { + let s = t.as_slice(); + let pk = s.get(1)?.clone(); + Some(vec!["mention".to_string(), pk]) + }) + .collect(); + mention_reference_tags(&mention_refs, &mut tags)?; imeta_tags(media_tags, &mut tags)?; emoji_tags(custom_emoji_tags, &mut tags)?; Ok(EventBuilder::new(Kind::Custom(40003), content).tags(tags)) @@ -964,6 +976,11 @@ mod tests { assert_eq!(tags[1][0], "e"); // The `p` tag rides right after the `e` tag (insertion order). assert_eq!(tags[2], vec!["p".to_string(), ALICE_HEX.to_string()]); + // Send-path parity: explicit mention ref for newly added @mentions (#2540). + assert!( + tags.iter().any(|t| t.as_slice() == ["mention", ALICE_HEX]), + "edit-added mention must emit mention ref tag, got {tags:?}" + ); } #[test]