Skip to content

feat(plugin): Buzz (Block/Nostr) platform adapter - #71610

Closed
rob-coco wants to merge 9 commits into
NousResearch:mainfrom
rob-coco:main
Closed

feat(plugin): Buzz (Block/Nostr) platform adapter#71610
rob-coco wants to merge 9 commits into
NousResearch:mainfrom
rob-coco:main

Conversation

@rob-coco

Copy link
Copy Markdown
Contributor

What

Adds a Buzz (Block/Nostr) platform adapter under plugins/platforms/buzz/, so the agent can connect to Buzz relays and take part in channels and DMs. Implements #68871.

Commits

  • feat(plugin): add Buzz (Block/Nostr) platform adapter
  • fix(plugin): bridge Buzz config.yaml → env + fix reaction flags
  • fix(plugin): strip leading @mention from Buzz channel messages
  • feat(plugin): add require_mention setting to Buzz adapter
  • fix(plugin): classify Buzz DMs by p-tag so un-mentioned DMs dispatch

Empty DM list workaround

The adapter works around an upstream Buzz bug I filed against the Buzz project — block/buzz#2897: buzz dms list returns [] for a DM recipient, so an agent can't discover DM conversations it belongs to. On hosted relays this means inbound DMs never appear in dms list; they only surface via channels list as an entry named "DM" with an empty description. That caused them to be seeded as group chats and fall behind the channel mention gate — so @Chip /whoami worked, but an un-mentioned DM was silently dropped.

Workaround (fix(plugin): classify Buzz DMs by p-tag …): instead of trusting dms list, classify from the Nostr tags of real traffic. A message another user sends in a DM carries a structural ["p", <own pubkey>] tag even when the text never @mentions the agent, whereas in a real channel a p-tag-to-self only ever accompanies a visible @mention. So a conversation latches to chat_type="dm" on the first kind-9 event that is p-tagged to self without a visible mention — guarded by channels list metadata so a real community channel (real name / non-empty description) is never reclassified. Latching also runs during history seeding (so a leaked DM bypasses the mention gate from the first poll after connect), dms list is kept as a best-effort source with a channels list fallback, and a leading @mention is stripped in DMs too so @Chip /whoami still fires as a slash command after reclassification.

Once block/buzz#2897 is fixed upstream, the dms list path works on its own and the tag-based classification becomes belt-and-suspenders.

Tests

tests/gateway/test_buzz_adapter.py covers the adapter, including the DM-classification behavior described above.

rob-coco and others added 5 commits July 25, 2026 22:13
Plugin-path adapter (zero core changes) connecting Hermes to a Buzz
community relay via the buzz CLI binary (JSON in/out, arg-list exec,
key passed via env only). Inbound uses a poll loop with per-channel
high-water marks seeded from newest (no history replay), event-id
de-dupe, self-echo suppression by pubkey, and mention gating in
channels (DMs always dispatch). Registers env_enablement, cron
home-channel delivery, and an out-of-process standalone sender,
mirroring the IRC plugin.

Verified against a live relay: connect -> send -> poll -> MessageEvent
round-trip, self-echo suppressed, clean disconnect.

Known limitation: polled inbound (default 4s); a websocket transport
(buzz-ws-client) is a future optimization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Buzz adapter's check_requirements() reads config from env only, so a
config.yaml-only setup (relay URL in gateway.platforms.buzz.extra) failed the
check_fn gate and was silently skipped at startup. Add an apply_yaml_config_fn
hook that bridges buzz.extra -> BUZZ_* env vars, mirroring the Slack/Telegram
pattern; BUZZ_PRIVATE_KEY stays a .env secret.

Also fix send_reaction() to use buzz-cli's real flags (--event <id> --emoji),
replacing the non-existent --channel/--message-id flags that would have failed
on every message. Verified live against the hosted relay (accepted:true).

Refs NousResearch#68871
Channel messages address the agent with a leading @mention (e.g. '@chip
/whoami'). The adapter passed the raw content through, so the gateway's
is_command() check (text.lstrip().startswith('/')) never matched and slash
commands were routed as plain chat. Strip a leading mention (name, npub, or
hex form) before dispatch in channels, mirroring the Discord adapter. Also
cleans normal prompts ('@chip what's up?' -> 'what's up?'). DMs are untouched.

Verified live: '@chip /whoami' -> '/whoami' after connect populates identity.

Refs NousResearch#68871
Channel mention-gating was hardcoded on. Add a configurable require_mention
(default True, preserving current behavior). When False, the agent responds to
every message in a watched channel, not only when @mentioned; DMs always
dispatch. Read from config.yaml gateway.platforms.buzz.extra.require_mention
with BUZZ_REQUIRE_MENTION env override, bridged via apply_yaml_config_fn like
the other settings. A leading mention is still stripped when present.

Refs NousResearch#68871
On hosted relays `buzz dms list` reliably returns [] even when DM
conversations exist, so DMs leaked in via `channels list` (as entries
named "DM" with an empty description) and were seeded chat_type="group".
That put them behind the channel mention gate: "@chip /whoami" worked
but an un-mentioned DM was silently dropped.

Classify from the Nostr tags of real traffic instead: a message another
user sends in a DM carries a structural ["p", <own pubkey>] tag even
when the text never mentions the agent, while in a real channel a
p-tag-to-self only ever accompanies a visible @mention (typed mention
or reply). A group conversation therefore latches to chat_type="dm" on
the first kind-9 event that is p-tagged to self WITHOUT a visible
mention in the content — guarded by channels-list metadata so a real
community channel (real name / non-empty description) is never
reclassified by a reply or mention that p-tags the agent.

- latch during history seeding too, so a leaked DM bypasses the
  mention gate from the very first poll after connect
- keep `dms list` as a best-effort source, and scan `channels list`
  as a fallback so DM conversations opened mid-run still get watched
- strip a leading @mention in DMs as well, so "@chip /whoami" keeps
  firing as a slash command after the conversation reclassifies

Refs NousResearch#68871

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rob-coco
rob-coco marked this pull request as draft July 25, 2026 22:17
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 25, 2026
@rob-coco

Copy link
Copy Markdown
Contributor Author

@alt-glitch would love to get your eyes on this. Buzz is growing pretty rapidly, and is shaping up to be a great tool for inter-agent communications.

@jethac

jethac commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Took your adapter for a real-world spin this weekend and wanted to report back — it held up well.

I've been building multi-agent support on top of #62944 (one gateway process, N agents), and Buzz is the first platform where that registry can map 1:1 onto workspace members — an identity is just a keypair. So I stacked a branch that runs one instance of your adapter per agent, each with its own nsec: draft PR #71686. Your five commits are cherry-picked verbatim with authorship preserved; the wiring builds strictly on top (per-agent credential resolution, connection-identity routing). Your 61 adapter tests pass unchanged on the stack — the per-agent settings path is additive, and legacy single-connection precedence is byte-identical when no agent is bound.

Two findings from running it against a live self-hosted relay (closed mode, NIP-42/98) that may be useful for this PR:

  1. Your fix(tools): treat Slack channel IDs as explicit send_message targets #2897 dms list fallback is load-bearing in the wild — recipient dms list returned [] on our relay too, and the p-tag classification path is what actually dispatched the DMs in our integration test. Good call building it.
  2. CLI-originated DMs never latch as DMs: messages sent via buzz messages send into a DM conversation carry only the h tag — the structural recipient p-tag your classifier keys on turns out to be stamped by the Buzz desktop client, not the relay. So DMs from CLI/headless senders fall through to the channel path. Possible softening: treat "conversation named DM + empty description + sender ≠ self" as a weaker classification signal, or push block/buzz to stamp the p-tag relay-side. Happy to file this upstream on block/buzz if useful.

One micro-observation, take or leave: _resolve_user_name hits users get per unknown pubkey with no negative caching — mostly invisible with one connection, but it amplifies with N pollers in one process. We didn't patch it in the stack to keep the diff clean.

Nice work on the adapter — it made the multi-agent demo almost anticlimactic.

rob-coco and others added 3 commits July 26, 2026 17:25
… adapter

Expands the 'Recommended display settings' section into a comprehensive
'Recommended default settings' block covering display, access control,
polling, and mention behavior. Each setting includes an inline annotation
and rationale bullet. Matches Telegram/email default behavior (no
intermediate tool output, mention-gated channels, private-by-default
access).

Refs NousResearch#68871
@rob-coco

Copy link
Copy Markdown
Contributor Author

2. CLI-originated DMs never latch as DMs: messages sent via buzz messages send into a DM conversation carry only the h tag — the structural recipient p-tag your classifier keys on turns out to be stamped by the Buzz desktop client, not the relay. So DMs from CLI/headless senders fall through to the channel path. Possible softening: treat "conversation named DM + empty description + sender ≠ self" as a weaker classification signal, or push block/buzz to stamp the p-tag relay-side. Happy to file this upstream on block/buzz if useful.

Yeah, I didn't want to do this because it felt like a hack to cue off of the name of a conversation. I've opened an issue with the block/buzz project to fix the dm channel array bug.

teknium1 added a commit that referenced this pull request Jul 28, 2026
…tration

Follow-up on the salvaged #71610 commits:
- acquire/release a scoped lock on relay_url+pubkey in connect/disconnect
  (IRC pattern) so two profiles can't drive one Buzz identity — duplicate
  replies and split de-dupe state; +2 tests
- negative-cache _resolve_user_name failures so a profile-less pubkey
  doesn't re-hit 'users get' every poll sweep (flagged by @jethac on the PR)
- register user-guide/messaging/buzz in website/sidebars.ts (page was
  unreachable — the #63359 trap)
teknium1 added a commit that referenced this pull request Jul 29, 2026
…tration

Follow-up on the salvaged #71610 commits:
- acquire/release a scoped lock on relay_url+pubkey in connect/disconnect
  (IRC pattern) so two profiles can't drive one Buzz identity — duplicate
  replies and split de-dupe state; +2 tests
- negative-cache _resolve_user_name failures so a profile-less pubkey
  doesn't re-hit 'users get' every poll sweep (flagged by @jethac on the PR)
- register user-guide/messaging/buzz in website/sidebars.ts (page was
  unreachable — the #63359 trap)
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #73610 with your authorship preserved on all six commits (adapter, config bridge, mention strip, require_mention, p-tag DM classification, docs defaults). On top we added a scoped identity lock (relay+pubkey — prevents the duplicate-reply collision two profiles could hit), negative caching in _resolve_user_name (from @jethac's review), and registered the docs page in sidebars.ts.

Really solid work — the p-tag DM classifier working around block/buzz#2897 was the standout piece, and both your hosted-relay verification and @jethac's independent self-hosted run gave us high confidence. Hermes now ships Buzz as a bundled platform. Thanks!

@teknium1 teknium1 closed this Jul 29, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…tration

Follow-up on the salvaged NousResearch#71610 commits:
- acquire/release a scoped lock on relay_url+pubkey in connect/disconnect
  (IRC pattern) so two profiles can't drive one Buzz identity — duplicate
  replies and split de-dupe state; +2 tests
- negative-cache _resolve_user_name failures so a profile-less pubkey
  doesn't re-hit 'users get' every poll sweep (flagged by @jethac on the PR)
- register user-guide/messaging/buzz in website/sidebars.ts (page was
  unreachable — the NousResearch#63359 trap)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants