Skip to content

fix(buzz-acp): never deliver ephemeral events to agent subprocesses - #4040

Open
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:claude/issue3649-20260731
Open

fix(buzz-acp): never deliver ephemeral events to agent subprocesses#4040
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:claude/issue3649-20260731

Conversation

@iroiro147

Copy link
Copy Markdown

Problem

When an agent runs subscribe=all (or any wildcard subscription rule) with no curated BUZZ_ACP_KINDS allowlist, the resolved rule set admits every kind on the wire — including ephemeral kinds:

  • Kind 20001 — presence updates
  • Kind 20002 — typing indicators
  • Kind 24200 — agent observer frames

Every keystroke in a channel emits dozens of these ephemeral events. Each one woke the agent subprocess into a full model turn: cost per keystroke for the operator and a false "agent is typing" signal in Desktop for whoever is watching.

Closes #3649.

Root Cause

resolve_channel_filters in config.rs resolves SubscribeMode::All with no kinds_override to kinds: None — a wildcard that matches every event kind. There is no design-level invariant that ephemeral events (kind 20000–29999, defined as "never stored" in buzz-core/src/kind.rs) should be excluded from agent delivery. The match_event function in filter.rs applied channel-scope, kind, mention, and expression filters — but had no concept of ephemeral exclusion.

Fix

Add a design-level invariant to the shared delivery filter (crates/buzz-acp/src/filter.rs, match_event): ephemeral events (kind 20000–29999) are never delivered to an agent subprocess, regardless of any subscription rule that might otherwise admit them.

This guard runs before the rule loop, so it protects:

  • SubscribeMode::All with no kinds_override (the reported case)
  • Any wildcard Config rule with empty kinds
  • Even an explicit All subscription that accidentally lists an ephemeral kind

Ephemeral events are transient by definition — they are not actionable work and must not wake an agent process. Failing closed here (never wake on any ephemeral kind) keeps wildcard subscriptions safe to use.

The guard uses the existing buzz_core::kind::is_ephemeral predicate (already a dependency of buzz-acp), so there is no new constant or duplicate logic.

Testing

New test test_match_event_ephemeral_never_wakes_agent_on_wildcard:

  • Builds a wildcard rule (empty kinds = match any kind) — the shape an All subscription with no curated list resolves to
  • Asserts that kind 20001 (presence), 20002 (typing), and 24200 (observer frame) all return None (no match)
  • Control: asserts that a non-ephemeral kind 9 (stream message) still matches the wildcard rule — the guard only rejects ephemeral kinds

Full buzz-acp lib test suite: 662 passed, 0 failed.

Backward Compatibility

Agents that explicitly subscribe to ephemeral kinds via BUZZ_ACP_KINDS will no longer receive them. This is the intended behavior — ephemeral events were never meant to be actionable, and any agent depending on them was paying a per-keystroke cost without realizing it. The fix is fail-closed: if an agent truly needs ambient channel activity, a future enhancement could add an opt-in include_ephemeral flag, but the default must be safe.

…lock#3649)

When an agent runs subscribe=all (or any wildcard rule) with no curated
BUZZ_ACP_KINDS allowlist, the resolved rule set admits every kind on the
wire — including kind 20001 presence updates, kind 20002 typing
indicators, and kind 24200 observer frames. Every keystroke in a channel
emits dozens of these ephemeral events, and each one woke the agent
subprocess into a full model turn: cost per keystroke for the operator
and a false "agent is typing" signal in Desktop for whoever is watching.

Add a design-level invariant to the shared delivery filter
crates/buzz-acp/src/filter.rs, match_event: ephemeral events
(20000–29999) are never delivered to an agent subprocess, regardless of
any subscription rule that might otherwise admit them. Ephemeral events
are transient by definition — they are not actionable work and must not
wake an agent process.

Closes block#3649.

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
@AhmedAburady

Copy link
Copy Markdown

Confirming this from a self-hosted deploy (buzz pinned at d40a3329, three agents on one
relay). The ephemeral guard is the right shape and it closes the keystroke half of #3649: we saw
1,063 relay rejections of invalid: reaction target event not found, which is agents trying to
👀 typing/presence events that were never stored. This PR removes that entirely.

One thing worth flagging while the PR is open, not as a request to widen it: the guard is scoped
to kinds 20000-29999, and in a multi-agent channel the larger burn comes from kinds 5 and
7, which are regular and stored. buzz-acp publishes 👀 at queue-push (lib.rs:2212) and 💬
before the prompt (pool.rs:1881), then ReactionGuard deletes both on turn end - four events
per picked-up event, all into the same channel, all delivered to any other wildcard subscriber.
So the agents wake on each other's bookkeeping and it does not converge.

On our relay that produced 25,717 kind-7 events against 304 real messages, with 22,373 of the
reactions targeting another reaction
. Roughly 20x the typing-indicator volume. It also does not
need respond_to=anyone: the author gate admits same-owner siblings in owner-only mode by
design, so two agents from one human on subscribe=all hit it on stock config.

I do not think that belongs in this PR - reactions can be genuinely actionable, unlike ambient
typing, so the fix is a different one. Filed separately as #4086 with the numbers and the source
trace. Raising it here only because after this merges subscribe=all will look fixed, and for
multi-agent setups the expensive half is still open.

Also, small correction for anyone using the workaround in #3649: the recommended
BUZZ_ACP_KINDS=9,40002,40003,5,7,1059,40100,40007,46010,46020 includes 5 and 7, which is
the storm fuel above. It is fine for a single agent; with more than one, dropping those two is
what actually stops it.

@iroiro147

Copy link
Copy Markdown
Author

@AhmedAburady Thanks for the field numbers — the 22,373 reaction-on-reaction count against 304 real messages quantifies the 'does not converge' claim better than my abstract argument could.

You were right that it didn't belong in this PR. The fix for the 5/7 storm half is now up separately as #4091 against your #4086: when BUZZ_ACP_KINDS is unset, both resolve_channel_filters() and resolve_dynamic_channel_filter() default to the curated list [9, 40002, 40003, 1059, 40007, 40100, 46010, 46020] — exactly the list you validated, minus 5 and 7. The BUZZ_ACP_KINDS override stays authoritative, so single-agent setups that deliberately want kinds 5/7 can still opt in.

Your point about the workaround list in #3649 being storm fuel for multi-agent setups is addressed the same way — once #4091 merges, dropping 5/7 becomes the default rather than a manual exclusion.

The ephemeral guard here (20000-29999) + the curated default in #4091 should cover both halves of the multi-agent wake-storm problem: ambient typing/presence and reactions/deletions.

@iroiro147

Copy link
Copy Markdown
Author

Thanks for the production confirmation — really useful to see it closing the rejection loop in the wild, and helpful to know the keystroke half specifically is what this heals.

On kinds 5 / 7 (stored reactions/deletion): agreed they're a larger class of cross-agent traffic, and the current guard (20000–29999 ephemeral per NIP-01) intentionally leaves them alone. Two reasons to keep this PR scoped narrowly: (a) kinds 5/7 are stored events — relay rejection on them isn't a typo-shaped invariant the way it is for ephemeral, it's a semantic question about whether agents should react/delete each other's content at all, which probably wants its own policy/gate; (b) widening the guard in this PR would mix a hard NIP-01 invariant with a design choice that deserves a separate discussion. Happy to open a follow-up issue if you'd like to take the 5/7 question explicitly — the deployment evidence you have (1,063 rejections on the narrow class) would be valuable context for that one too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

buzz-acp subscribe=all delivers typing/presence events to agents: token burn per keystroke and false typing badge

2 participants