Skip to content

buzz-acp: SubscribeMode::All subscribes to every event kind — typing indicators (kind:20002) open agent turns and cancel pending wakeups #4949

Open

Description

@TolgaCinisli

Environment

  • Buzz Desktop v0.5.5 (8342dfcc), bundled buzz-acp
  • Managed agent, backend: local, Claude runtime (claude-agent-acp 0.65.0)
  • BUZZ_ACP_SUBSCRIBE=all (set so the agent answers the owner without requiring a mention)

Observed

With subscribe=all, the agent's turns are repeatedly triggered by kind:20002 typing indicators (empty content, ephemeral):

  1. Four separate turns in one session were opened by typing events — the user only started typing; the actual kind:9 message arrived mid-turn as a separate notification.
  2. The harness passed the ephemeral event as the trigger and set it as the reply target (--reply-to <20002 event id>). Ephemeral events are not persisted, so replies would thread against a nonexistent root.
  3. A pending scheduled wakeup (Claude Code ScheduleWakeup, due in 109s) was at risk of cancellation when a typing indicator opened a turn: pending wakeups are cancelled on incoming user activity by design, but here pressing a key — without ever sending a message — was enough to count as user activity.

Root cause

crates/buzz-acp/src/config.rs, subscription filter construction:

  • SubscribeMode::Mentions defaults to an explicit kind list: [KIND_STREAM_MESSAGE (9), KIND_WORKFLOW_APPROVAL_REQUESTED (46010), KIND_STREAM_REMINDER (40007)].
  • SubscribeMode::All builds ChannelFilter { kinds: config.kinds_override.clone(), require_mention: false } — when BUZZ_ACP_KINDS is not set, kinds is None, which the relay subscription treats as wildcard (all kinds).

So "all" silently changes meaning from "all messages (no mention required)" to "all event kinds", pulling in ephemeral kinds (typing 20002, etc.) as turn triggers.

Expected

All should mean "the same message kinds as Mentions, without the mention requirement". Suggested fix:

SubscribeMode::All => {
    let kinds = config.kinds_override.clone().unwrap_or_else(|| vec![
        KIND_STREAM_MESSAGE,
        KIND_WORKFLOW_APPROVAL_REQUESTED,
        KIND_STREAM_REMINDER,
    ]);
    // kinds: Some(kinds), require_mention: false
}

Impact

  • Every keystroke-start burns an agent turn (quota + noise for subscription-billed runtimes).
  • Scheduled wakeups die when the user touches the keyboard — breaks any agent that timers long-running work.
  • Replies threaded to ephemeral events produce broken threads.

Workaround

BUZZ_ACP_KINDS=9,40007,46010 alongside BUZZ_ACP_SUBSCRIBE=all restores the intended behavior.

Activity

  1. added 20 commits that reference this issue on Aug 7, 2026
  2. dziduchm commented on Sep 23, 2026

    @dziduchm

    Operator here with a 24h measured-cost datapoint that confirms and quantifies the impact section. This run used a different runtime stack than the OP (Hermes runtime on MiniMax-M3 via buzz-acp 0.5.23, BUZZ_ACP_SUBSCRIBE=all, default kinds override unset), so the wildcard behavior reproduces across runtimes, not just claude-agent-acp.

    Measured cost (from session_model_usage in our agent's state.db)

    Window 2026-09-22, 24h, one hive, one agent:

    • 670 sessions started, 865 API calls. 654 of the 670 sessions had exactly 1 API call (pure typing-indicator bootstrap cycles: inbound kind:20002, session spawn, base prompt, canned "no actionable content" reply, teardown).
    • Totals for the day: 1,014,507 input tokens, 8,756,507 cache-read tokens, 83,556 output tokens.
    • Single-cycle cost is the input bill: one logged bootstrap call measured 73,404 tokens (msgs=2). That call is also where the day ended: the provider returned 429 Token Plan usage limit reached (2056), and the retry loop around it burned the rest of the plan's headroom.
    • 2026-09-23 (partial window to ~07:03 UTC, after we started shutting things down): still 65 sessions / 119 calls / 104,120 input / 2,758,777 cache-read before we intervened.

    Two things worth flagging for anyone prioritizing this:

    1. The cache-read number is not savings here. Because every cycle is a fresh session (lazy pool child), the base prompt is re-read from cache once per cycle at best; 8.7M cache-read tokens/day is the loop charging the plan's cache budget continuously, and the 1M+ uncached input/day is on top of it.
    2. The trigger cadence is not human typing. Our inbound firehose ran ~20 events/min sustained for hours with the same shape every time: kind:20002, content: "", tags: [["h", <channel>]]. Whatever the client-side keepalive is, subscribe=all turns it into a session bootstrap roughly every 3 seconds.

    The BUZZ_ACP_KINDS=9,40007,46010 workaround from the OP held for us too, but it is discover-only: nothing in the docs or logs suggests "all" means "all event kinds", and the failure mode is invisible until the provider plan dies. A debug-level log line on wildcard subscriptions including ephemeral kinds (or an outright denylist for 20002 in the All default per the OP's suggested fix) would have saved us a plan a day.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions