diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index e6164b02dd..3345ee53df 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -222,16 +222,16 @@ Start with **N=2** for most deployments. Increase if queue depth grows under loa ## Forum Channels -By default, the ACP harness subscribes to stream message kinds (9, 46010, 40007). To receive forum events, opt in with `--kinds` and disable the mention filter (forum posts don't @mention agents): +By default, the ACP harness subscribes to actionable stream kinds (9 messages, 40003 edits that add a mention, 46010 workflow approvals, and 40007 reminders). To receive forum events, opt in with `--kinds` and disable the mention filter (forum posts don't @mention agents): **CLI flags:** ```bash -buzz-acp --kinds 9,46010,40007,45001,45002,45003 --no-mention-filter +buzz-acp --kinds 9,40003,46010,40007,45001,45002,45003 --no-mention-filter ``` **Or with `--subscribe all`:** ```bash -buzz-acp --subscribe all --kinds 9,46010,40007,45001,45002,45003 +buzz-acp --subscribe all --kinds 9,40003,46010,40007,45001,45002,45003 ``` **Per-channel config:** diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index 35aaec188d..82e0787ad8 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -6,6 +6,10 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; +use buzz_core::kind::{ + KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_REMINDER, + KIND_WORKFLOW_APPROVAL_REQUESTED, +}; use clap::Parser; use clap::ValueEnum; use nostr::Keys; @@ -1237,16 +1241,26 @@ pub fn load_rules(path: &std::path::Path) -> Result, Confi Ok(config.rules) } +/// Event kinds that carry actionable direct mentions by default. +/// +/// Message edits are included because Desktop emits `p` tags only for +/// recipients newly added by an edit. Receiving kind 40003 therefore wakes an +/// agent once for a newly added mention without re-waking it for ordinary edits. +pub(crate) fn default_mention_kinds() -> Vec { + vec![ + KIND_STREAM_MESSAGE, + KIND_STREAM_MESSAGE_EDIT, + KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_REMINDER, + ] +} + /// Resolve per-channel NIP-01 filters from config + discovered channels. pub fn resolve_channel_filters( config: &Config, discovered_channels: &[Uuid], rules: &[SubscriptionRule], ) -> HashMap { - use buzz_core::kind::{ - KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, - }; - let target_channels: Vec = if let Some(ref overrides) = config.channels_override { overrides .iter() @@ -1261,13 +1275,10 @@ pub fn resolve_channel_filters( match config.subscribe_mode { SubscribeMode::Mentions => { - let kinds = config.kinds_override.clone().unwrap_or_else(|| { - vec![ - KIND_STREAM_MESSAGE, - KIND_WORKFLOW_APPROVAL_REQUESTED, - KIND_STREAM_REMINDER, - ] - }); + let kinds = config + .kinds_override + .clone() + .unwrap_or_else(default_mention_kinds); let require_mention = !config.no_mention_filter; for ch in &target_channels { result.insert( @@ -1345,10 +1356,6 @@ pub fn resolve_dynamic_channel_filter( channel_id: Uuid, rules: &[crate::filter::SubscriptionRule], ) -> Option { - use buzz_core::kind::{ - KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, - }; - // In Mentions/All mode, if the operator explicitly constrained channels // with --channels, only allow dynamic subscription to channels in that // allowlist. Config mode ignores --channels (per CLI contract) and uses @@ -1366,13 +1373,12 @@ pub fn resolve_dynamic_channel_filter( match config.subscribe_mode { SubscribeMode::Mentions => Some(ChannelFilter { - kinds: Some(config.kinds_override.clone().unwrap_or_else(|| { - vec![ - KIND_STREAM_MESSAGE, - KIND_WORKFLOW_APPROVAL_REQUESTED, - KIND_STREAM_REMINDER, - ] - })), + kinds: Some( + config + .kinds_override + .clone() + .unwrap_or_else(default_mention_kinds), + ), require_mention: !config.no_mention_filter, }), SubscribeMode::All => Some(ChannelFilter { @@ -1516,11 +1522,28 @@ mod tests { assert!(f.require_mention, "mentions mode requires mention"); let kinds = f.kinds.as_ref().expect("should have kinds"); assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE)); + assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE_EDIT)); assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED)); assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER)); } } + #[test] + fn test_dynamic_mentions_mode_includes_message_edits() { + let config = test_config(SubscribeMode::Mentions); + let filter = resolve_dynamic_channel_filter(&config, Uuid::new_v4(), &[]) + .expect("dynamic channel should be subscribed"); + + assert!(filter.require_mention); + assert!( + filter + .kinds + .expect("mentions mode should constrain kinds") + .contains(&KIND_STREAM_MESSAGE_EDIT), + "newly mentioned agents must receive message edits on dynamic channels" + ); + } + #[test] fn test_mentions_mode_custom_kinds() { let mut config = test_config(SubscribeMode::Mentions); diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 811253e4ac..2e7971bce4 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -22,7 +22,6 @@ 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, }; use buzz_core::observer::{ decrypt_observer_payload, encrypt_observer_payload, OBSERVER_FRAME_TELEMETRY, @@ -1492,13 +1491,10 @@ async fn tokio_main() -> Result<()> { vec![SubscriptionRule { name: "mentions".into(), channels: filter::ChannelScope::All("all".into()), - kinds: config.kinds_override.clone().unwrap_or_else(|| { - vec![ - KIND_STREAM_MESSAGE, - KIND_WORKFLOW_APPROVAL_REQUESTED, - KIND_STREAM_REMINDER, - ] - }), + kinds: config + .kinds_override + .clone() + .unwrap_or_else(config::default_mention_kinds), require_mention: !config.no_mention_filter, filter: None, compiled_filter: None, diff --git a/crates/buzz-acp/src/setup_mode.rs b/crates/buzz-acp/src/setup_mode.rs index b1a9372ea4..74073723d0 100644 --- a/crates/buzz-acp/src/setup_mode.rs +++ b/crates/buzz-acp/src/setup_mode.rs @@ -37,7 +37,7 @@ use std::collections::HashSet; use anyhow::Result; use buzz_core::kind::{ KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_STREAM_MESSAGE, - KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_MESSAGE_EDIT, KIND_WORKFLOW_APPROVAL_REQUESTED, }; use nostr::EventId; use serde::{Deserialize, Serialize}; @@ -410,7 +410,7 @@ pub(crate) async fn run_setup_listener(config: Config, payload: SetupPayload) -> } // Ignore non-message kinds (relay housekeeping, etc.). - if kind_u32 != KIND_STREAM_MESSAGE && kind_u32 != KIND_WORKFLOW_APPROVAL_REQUESTED { + if !is_setup_nudge_kind(kind_u32) { continue; } @@ -512,6 +512,13 @@ pub(crate) fn should_nudge_for_event( true } +fn is_setup_nudge_kind(kind: u32) -> bool { + matches!( + kind, + KIND_STREAM_MESSAGE | KIND_STREAM_MESSAGE_EDIT | KIND_WORKFLOW_APPROVAL_REQUESTED + ) +} + /// Build the subscription rules used in setup mode. /// /// Always uses "mentions" mode: setup mode must not react to every event. @@ -524,7 +531,7 @@ fn build_setup_subscription_rules(config: &Config) -> Vec