From ddad9b26f60476ba6062c49bcc21ca9d4c1d795d Mon Sep 17 00:00:00 2001 From: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f Date: Wed, 5 Aug 2026 17:43:09 -0500 Subject: [PATCH 1/2] fix(acp): stop SubscribeMode::All from subscribing to every event kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SubscribeMode::All built ChannelFilter with kinds: config.kinds_override.clone(), which is None when BUZZ_ACP_KINDS is not set. The relay treats None as a wildcard, so typing indicators (kind 20002) and other ephemeral kinds opened agent turns and cancelled pending scheduled wakeups — every keystroke burned quota without the user ever sending a message. All should mean the same message kinds as Mentions (9, 40007, 46010) without the mention requirement, not all event kinds. Apply the same unwrap_or_else default in both resolve_channel_filters and resolve_dynamic_channel_filter. Update the test that asserted the old wildcard behavior and add a regression test for the dynamic filter path. Refs #4949 Co-authored-by: Brad Groux Signed-off-by: Brad Groux Signed-off-by: npub17q2gdupkvswvk5kprwc7plergm4gn295uw6fe4mjyjv53ahuhtnq02jd3f --- crates/buzz-acp/src/config.rs | 51 +++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index d959685846..e26ae001b1 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -1274,11 +1274,18 @@ pub fn resolve_channel_filters( } } SubscribeMode::All => { + let kinds = config.kinds_override.clone().unwrap_or_else(|| { + vec![ + KIND_STREAM_MESSAGE, + KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_REMINDER, + ] + }); for ch in &target_channels { result.insert( *ch, ChannelFilter { - kinds: config.kinds_override.clone(), + kinds: Some(kinds.clone()), require_mention: false, }, ); @@ -1370,7 +1377,13 @@ pub fn resolve_dynamic_channel_filter( require_mention: !config.no_mention_filter, }), SubscribeMode::All => Some(ChannelFilter { - kinds: config.kinds_override.clone(), + kinds: Some(config.kinds_override.clone().unwrap_or_else(|| { + vec![ + KIND_STREAM_MESSAGE, + KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_REMINDER, + ] + })), require_mention: false, }), SubscribeMode::Config => { @@ -1763,7 +1776,12 @@ mod tests { } #[test] - fn test_all_mode_wildcard() { + fn test_all_mode_defaults_to_message_kinds() { + // SubscribeMode::All should mean "same kinds as Mentions, without + // the mention requirement" — not "all event kinds" (wildcard). + // Without this default, typing indicators (kind 20002) and other + // ephemeral kinds open agent turns and cancel pending wakeups. + // See #4949. let config = test_config(SubscribeMode::All); let channels = vec![Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()]; let result = resolve_channel_filters(&config, &channels, &[]); @@ -1771,11 +1789,11 @@ mod tests { assert_eq!(result.len(), 3); for ch in &channels { let f = result.get(ch).unwrap(); - assert!( - f.kinds.is_none(), - "all mode with no override = wildcard kinds" - ); - assert!(!f.require_mention); + let kinds = f.kinds.as_ref().expect("All mode must have explicit kinds"); + assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE), "must include kind:9"); + assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED), "must include kind:46010"); + assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER), "must include kind:40007"); + assert!(!f.require_mention, "All mode must not require mention"); } } @@ -1790,6 +1808,23 @@ mod tests { assert_eq!(f.kinds.as_ref().unwrap(), &[9, 7]); } + #[test] + fn test_all_mode_dynamic_filter_defaults_to_message_kinds() { + // resolve_dynamic_channel_filter must also default to the message + // kinds list when BUZZ_ACP_KINDS is not set, not wildcard. + // See #4949. + let config = test_config(SubscribeMode::All); + let ch = Uuid::new_v4(); + let result = resolve_dynamic_channel_filter(&config, ch, &[]); + + let f = result.expect("All mode must return a filter"); + let kinds = f.kinds.as_ref().expect("All mode must have explicit kinds"); + assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE), "must include kind:9"); + assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED), "must include kind:46010"); + assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER), "must include kind:40007"); + assert!(!f.require_mention, "All mode must not require mention"); + } + #[test] fn test_channels_override_filters_to_discovered() { let mut config = test_config(SubscribeMode::All); From 3aeeb443a72fff0c40d40afc4aeb8257321dc33d Mon Sep 17 00:00:00 2001 From: Brad Groux <3053586+BradGroux@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:26:39 -0500 Subject: [PATCH 2/2] fix(acp): keep all-mode rules on message kinds Reuse one bounded default for initial and dynamic relay filters and runtime rule matching so typing indicators cannot reach the turn matcher through an inconsistent wildcard rule. Co-authored-by: Brad Groux Signed-off-by: Brad Groux Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com> --- crates/buzz-acp/src/config.rs | 83 ++++++++++++++++------------------- crates/buzz-acp/src/lib.rs | 17 ++++--- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index e26ae001b1..661f16a780 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -35,6 +35,22 @@ pub(crate) const DEFAULT_MAX_TURN_DURATION_SECS: u64 = 7200; /// deadline (`max_turn_duration + IN_FLIGHT_DEADLINE_BUFFER_SECS`). pub(crate) const MAX_TURN_DURATION_CEILING_SECS: u64 = 604_800; +/// Event kinds that open agent turns unless an operator supplies an override. +/// +/// `mentions` and `all` differ only in whether a `p` tag is required; both +/// modes subscribe to and match this same bounded set by default. +pub(crate) fn default_subscription_kinds() -> Vec { + use buzz_core::kind::{ + KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, + }; + + vec![ + KIND_STREAM_MESSAGE, + KIND_WORKFLOW_APPROVAL_REQUESTED, + KIND_STREAM_REMINDER, + ] +} + #[derive(Debug, Error)] pub enum ConfigError { #[error("failed to parse nostr keys: {0}")] @@ -1237,10 +1253,6 @@ pub fn resolve_channel_filters( 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() @@ -1255,13 +1267,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_subscription_kinds); let require_mention = !config.no_mention_filter; for ch in &target_channels { result.insert( @@ -1274,13 +1283,10 @@ pub fn resolve_channel_filters( } } SubscribeMode::All => { - 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_subscription_kinds); for ch in &target_channels { result.insert( *ch, @@ -1346,10 +1352,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 @@ -1367,23 +1369,21 @@ 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_subscription_kinds), + ), require_mention: !config.no_mention_filter, }), SubscribeMode::All => 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_subscription_kinds), + ), require_mention: false, }), SubscribeMode::Config => { @@ -1785,14 +1785,12 @@ mod tests { let config = test_config(SubscribeMode::All); let channels = vec![Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()]; let result = resolve_channel_filters(&config, &channels, &[]); + let expected = default_subscription_kinds(); assert_eq!(result.len(), 3); for ch in &channels { let f = result.get(ch).unwrap(); - let kinds = f.kinds.as_ref().expect("All mode must have explicit kinds"); - assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE), "must include kind:9"); - assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED), "must include kind:46010"); - assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER), "must include kind:40007"); + assert_eq!(f.kinds.as_deref(), Some(expected.as_slice())); assert!(!f.require_mention, "All mode must not require mention"); } } @@ -1818,10 +1816,7 @@ mod tests { let result = resolve_dynamic_channel_filter(&config, ch, &[]); let f = result.expect("All mode must return a filter"); - let kinds = f.kinds.as_ref().expect("All mode must have explicit kinds"); - assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_MESSAGE), "must include kind:9"); - assert!(kinds.contains(&buzz_core::kind::KIND_WORKFLOW_APPROVAL_REQUESTED), "must include kind:46010"); - assert!(kinds.contains(&buzz_core::kind::KIND_STREAM_REMINDER), "must include kind:40007"); + assert_eq!(f.kinds, Some(default_subscription_kinds())); assert!(!f.require_mention, "All mode must not require mention"); } diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 0c4e5f158c..1ad5c2633b 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_subscription_kinds), require_mention: !config.no_mention_filter, filter: None, compiled_filter: None, @@ -1510,7 +1506,10 @@ async fn tokio_main() -> Result<()> { vec![SubscriptionRule { name: "all".into(), channels: filter::ChannelScope::All("all".into()), - kinds: config.kinds_override.clone().unwrap_or_default(), + kinds: config + .kinds_override + .clone() + .unwrap_or_else(config::default_subscription_kinds), require_mention: false, filter: None, compiled_filter: None,