Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
67 changes: 45 additions & 22 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1237,16 +1241,26 @@ pub fn load_rules(path: &std::path::Path) -> Result<Vec<SubscriptionRule>, 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<u32> {
vec![
KIND_STREAM_MESSAGE,
KIND_STREAM_MESSAGE_EDIT,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve edit mentions to the original message before prompting

When a kind 40003 event wakes the harness, its bare e tag identifies the edited message, but parse_thread_tags only recognizes marker-based NIP-10 tags. The edit is therefore treated as a top-level message, and the prompt tells the agent to reply with --reply-to <edit-event-id>; the CLI then roots that reply at the auxiliary edit rather than the visible original message. Such replies do not appear in the original thread, and the setup listener has the same problem when publishing its nudge. Resolve the edit target and use the original message/thread as the context and reply anchor before forwarding these events.

Useful? React with 👍 / 👎.

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<Uuid, ChannelFilter> {
use buzz_core::kind::{
KIND_STREAM_MESSAGE, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED,
};

let target_channels: Vec<Uuid> = if let Some(ref overrides) = config.channels_override {
overrides
.iter()
Expand All @@ -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(
Expand Down Expand Up @@ -1345,10 +1356,6 @@ pub fn resolve_dynamic_channel_filter(
channel_id: Uuid,
rules: &[crate::filter::SubscriptionRule],
) -> Option<ChannelFilter> {
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
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 4 additions & 8 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 22 additions & 3 deletions crates/buzz-acp/src/setup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
Expand All @@ -524,7 +531,7 @@ fn build_setup_subscription_rules(config: &Config) -> Vec<filter::SubscriptionRu
let kinds = config
.kinds_override
.clone()
.unwrap_or_else(|| vec![KIND_STREAM_MESSAGE, KIND_WORKFLOW_APPROVAL_REQUESTED]);
.unwrap_or_else(crate::config::default_mention_kinds);

match &config.subscribe_mode {
// Config mode: load the actual rules, but they will be filtered by
Expand Down Expand Up @@ -699,6 +706,18 @@ mod tests {
));
}

#[test]
fn setup_listener_defaults_include_newly_mentioned_message_edits() {
assert!(
crate::config::default_mention_kinds().contains(&KIND_STREAM_MESSAGE_EDIT),
"setup listener shares the normal actionable-mention defaults"
);
assert!(
is_setup_nudge_kind(KIND_STREAM_MESSAGE_EDIT),
"setup listener must process a delivered mention edit"
);
}

#[test]
fn nudge_body_names_all_requirements() {
let payload = SetupPayload {
Expand Down
Loading