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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ RUST_LOG=buzz_relay=debug,buzz_db=debug,buzz_auth=debug,buzz_pubsub=debug,tower_
# Set to true to disable the @-mention filter in mentions mode.
# BUZZ_ACP_NO_MENTION_FILTER=false

# Follow unmentioned replies in threads the agent participates in (default).
# Set to true to restore strict mention-per-message behavior.
# BUZZ_ACP_NO_FOLLOW_THREADS=false

# Follow-only author policy: "humans" (safe default) or "all". Under
# "humans", agent and unknown identities still require an explicit mention.
# BUZZ_ACP_FOLLOW_THREAD_AUTHORS=humans

# Sliding inactivity TTL for followed thread roots, in seconds (24 hours).
# BUZZ_ACP_THREAD_FOLLOW_TTL_SECS=86400

# Path to TOML config file for rule-based subscriptions (config mode).
# BUZZ_ACP_CONFIG=./buzz-acp.toml

Expand Down
20 changes: 20 additions & 0 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@ Owner control commands must be kind:9 stream messages from the owner, must menti

> **Note:** The default mode is `owner-only`. Agents without a registered `agent_owner_pubkey` will not respond to any events until the owner is resolved. Set `--respond-to anyone` to disable the gate entirely.

### Followed Threads

In the default `mentions` subscription mode, a mention starts following that
specific thread. Subsequent unmentioned replies in the thread can trigger the
agent until the sliding inactivity TTL expires. Other channel messages remain
mention-gated.

| Flag | Env Var | Default | Description |
|------|---------|---------|-------------|
| `--no-follow-threads` | `BUZZ_ACP_NO_FOLLOW_THREADS` | `false` | Restore strict mention-per-message behavior. |
| `--follow-thread-authors` | `BUZZ_ACP_FOLLOW_THREAD_AUTHORS` | `humans` | `humans` admits only positively classified human members; `all` also admits agent/unknown authors and accepts auto-continuation risk. Explicit mentions are unaffected. |
| `--thread-follow-ttl-secs` | `BUZZ_ACP_THREAD_FOLLOW_TTL_SECS` | `86400` | Sliding inactivity TTL for followed roots. |

Follow state is in-memory, isolated per channel, capped at 64 roots per
channel, and cleared when the agent leaves the channel. The safe `humans`
policy fails closed: a valid NIP-OA attestation or channel `bot` role proves an
agent, normal member roles prove a human, and unresolved identities still need
an explicit mention. Use `--follow-thread-authors all` only when every admitted
author is trusted and agent-to-agent auto-continuation is intentional.

**Examples:**

```bash
Expand Down
63 changes: 63 additions & 0 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ pub enum SubscribeMode {
Config,
}

/// Author policy for followed-thread admissions (#2270 loop guard).
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum FollowThreadAuthors {
/// Admit unmentioned followed-thread replies from humans only (default).
Humans,
/// Admit unmentioned followed-thread replies from any author, including
/// agents. Opting in accepts the risk of agent↔agent auto-continuation.
All,
}

#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum DedupMode {
Drop,
Expand Down Expand Up @@ -338,6 +348,34 @@ pub struct CliArgs {
#[arg(long, env = "BUZZ_ACP_NO_MENTION_FILTER")]
pub no_mention_filter: bool,

/// Disable thread-following. With this set, a mention-gated agent only
/// sees messages that explicitly @mention it — even untagged replies in
/// threads it is already participating in (the pre-#2270 behavior).
#[arg(long, env = "BUZZ_ACP_NO_FOLLOW_THREADS")]
pub no_follow_threads: bool,

/// Whose unmentioned replies a followed thread admits. `humans` (default)
/// admits human authors only — agent-authored replies still require an
/// explicit @mention, so two agents sharing a thread can never
/// auto-continue each other. `all` admits any author.
#[arg(
long,
env = "BUZZ_ACP_FOLLOW_THREAD_AUTHORS",
default_value = "humans",
value_enum
)]
pub follow_thread_authors: FollowThreadAuthors,

/// Sliding inactivity TTL, in seconds, for followed threads. A thread the
/// agent participated in stops being followed after this long without
/// activity; a fresh @mention re-joins it.
#[arg(
long,
env = "BUZZ_ACP_THREAD_FOLLOW_TTL_SECS",
default_value_t = 86_400
)]
pub thread_follow_ttl_secs: u64,

#[arg(long, env = "BUZZ_ACP_CONFIG", default_value = "./buzz-acp.toml")]
pub config: PathBuf,

Expand Down Expand Up @@ -486,6 +524,12 @@ pub struct ChannelFilter {
pub kinds: Option<Vec<u32>>,
/// Whether to include `#p` tag filter for agent pubkey.
pub require_mention: bool,
/// Thread roots the agent follows in this channel. When non-empty and
/// `require_mention` is set, the REQ carries a second filter clause
/// (`#h` + `#e`) so untagged replies in these threads are delivered
/// alongside explicit mentions. Ignored when `require_mention` is false
/// (the mention-less clause already delivers everything).
pub thread_roots: Vec<String>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -515,6 +559,13 @@ pub struct Config {
pub kinds_override: Option<Vec<u32>>,
pub channels_override: Option<Vec<String>>,
pub no_mention_filter: bool,
/// Whether the agent follows threads it has participated in (#2270).
/// Only consulted in Mentions mode — All mode delivers everything anyway.
pub follow_threads: bool,
/// Sliding inactivity TTL for followed thread roots, in seconds.
pub thread_follow_ttl_secs: u64,
/// Author policy for followed-thread admissions (loop guard).
pub follow_thread_authors: FollowThreadAuthors,
pub config_path: PathBuf,
pub context_message_limit: u32,
/// Maximum turns per session before proactive rotation. 0 = disabled.
Expand Down Expand Up @@ -1056,6 +1107,9 @@ impl Config {
kinds_override: args.kinds,
channels_override: args.channels,
no_mention_filter: args.no_mention_filter,
follow_threads: !args.no_follow_threads,
thread_follow_ttl_secs: args.thread_follow_ttl_secs,
follow_thread_authors: args.follow_thread_authors,
config_path: args.config,
context_message_limit: args.context_message_limit,
max_turns_per_session: args.max_turns_per_session,
Expand Down Expand Up @@ -1243,6 +1297,7 @@ pub fn resolve_channel_filters(
ChannelFilter {
kinds: Some(kinds.clone()),
require_mention,
thread_roots: Vec::new(),
},
);
}
Expand All @@ -1254,6 +1309,7 @@ pub fn resolve_channel_filters(
ChannelFilter {
kinds: config.kinds_override.clone(),
require_mention: false,
thread_roots: Vec::new(),
},
);
}
Expand Down Expand Up @@ -1289,6 +1345,7 @@ pub fn resolve_channel_filters(
ChannelFilter {
kinds: merged_kinds,
require_mention,
thread_roots: Vec::new(),
},
);
}
Expand Down Expand Up @@ -1342,10 +1399,12 @@ pub fn resolve_dynamic_channel_filter(
]
})),
require_mention: !config.no_mention_filter,
thread_roots: Vec::new(),
}),
SubscribeMode::All => Some(ChannelFilter {
kinds: config.kinds_override.clone(),
require_mention: false,
thread_roots: Vec::new(),
}),
SubscribeMode::Config => {
// Same merge logic as resolve_channel_filters() Config branch:
Expand Down Expand Up @@ -1383,6 +1442,7 @@ pub fn resolve_dynamic_channel_filter(
Some(ChannelFilter {
kinds: merged_kinds,
require_mention,
thread_roots: Vec::new(),
})
}
}
Expand Down Expand Up @@ -1429,6 +1489,9 @@ mod tests {
kinds_override: None,
channels_override: None,
no_mention_filter: false,
follow_threads: true,
thread_follow_ttl_secs: 86_400,
follow_thread_authors: FollowThreadAuthors::Humans,
config_path: PathBuf::from("./buzz-acp.toml"),
context_message_limit: 12,
max_turns_per_session: 0,
Expand Down
Loading