Skip to content

fix(buzz): resolve outbound @mentions to member pubkeys - #74830

Closed
fraserpllc wants to merge 1 commit into
NousResearch:mainfrom
fraserpllc:fix/buzz-outbound-mention-resolution
Closed

fix(buzz): resolve outbound @mentions to member pubkeys#74830
fraserpllc wants to merge 1 commit into
NousResearch:mainfrom
fraserpllc:fix/buzz-outbound-mention-resolution

Conversation

@fraserpllc

Copy link
Copy Markdown

What does this PR do?

buzz-cli rejects an entire send when @Name text does not match a current channel member:

user_error: mention '@-mention' does not match a current channel member;
retry with --mention <pubkey> (exit 1)

The adapter never passed --mention, so any agent-authored message naming another participant failed outright — including the plain-text fallback, leaving the message undelivered:

WARNING [Buzz] Send failed: user_error: mention '@-mention' does not match a
        current channel member; retry with --mention <pubkey> (exit 1)
        — trying plain-text fallback
ERROR   [Buzz] Fallback send also failed: user_error: mention '@-mention' does
        not match a current channel member; retry with --mention <pubkey>

This blocks agent-to-agent coordination in shared channels, where addressing another agent by name is the normal interaction. Hermes composed a 981-character reply naming two other agents and none of it reached the channel.

The fix resolves @Name tokens against the channel roster and passes the matched identities as repeated --mention arguments. Per the CLI contract, "supplying any explicit identity permits unresolved or ambiguous @name text as presentation-only" — so partial resolution still averts a hard failure.

Related Issue

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • plugins/platforms/buzz/adapter.py
    • New module-level _resolve_mention_pubkeys() helper: extracts @Name tokens, resolves them against channels members, returns hex pubkeys.
    • Wired into send(), send_image() captions, and _standalone_send() (cron delivery) — one shared helper, no duplicated logic.
    • Thin BuzzAdapter._resolve_mentions() wrapper supplies the adapter's cached name resolver and self-pubkey.
  • tests/gateway/test_buzz_adapter.py
    • New TestOutboundMentionResolution class, 11 cases.

Behaviour details worth calling out:

Input Result
@Honey (unique member) resolved → --mention <pubkey>
@Twin (two members share the label) left unresolved — never notify the wrong member
@Ghost (not a member) presentation-only, send succeeds
raw hex / npub1… passed through, no roster lookup
own display name dropped (no self-mention)
bob@example.com not treated as a mention
text with no @ roster lookup skipped entirely
roster lookup fails returns []; send proceeds exactly as before

channels members returns only {pubkey, role} — no display names — so labels come from the existing cached _resolve_user_name profile lookup (which already negative-caches misses). Inline name fields are preferred first, so this keeps working if a future CLI version starts returning them.

How to Test

  1. Two agents in one Buzz channel (e.g. Hermes and Honey).
  2. From the Hermes gateway, send a message naming the other agent:
    await adapter.send(CHANNEL_UUID, "@Honey please review the draft")
  3. Before: fails with mention '@-mention' does not match a current channel member, fallback also fails, nothing delivered.
    After: delivered, with Honey notified.

Automated:

scripts/run_tests.sh tests/gateway/test_buzz_adapter.py tests/gateway/test_buzz_websocket.py
=== Summary: 2 files, 38 tests passed, 0 failed ===

Verified live against a self-hosted relay (wss://…communities.buzz.xyz) — the exact send that previously failed now posts successfully with both mentions resolved.

Mutation-checked: reverting just the send() wiring fails 5 of the new tests, confirming they bind to the behaviour rather than passing vacuously.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the test suite and all tests pass
  • I've added tests for my changes
  • I've tested on my platform: macOS 26.5.2 (Apple Silicon), Python 3.11.15

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (no user-facing config or API change)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md — N/A
  • I've considered cross-platform impact — no platform-specific code (pure string/list handling over the existing CLI wrapper)
  • I've updated tool descriptions/schemas — N/A

Notes for reviewers

  • Not a duplicate of fix(buzz): require explicit display-name mentions #74467. That PR fixes inbound mention gating (_is_mentioned waking an agent on a bare name); this fixes the outbound send path. They touch different functions and are complementary — happy to rebase if fix(buzz): require explicit display-name mentions #74467 lands first.
  • Full-suite context: tests/gateway/ reports 89 failures on my machine both with and without this change (verified via git stash) — all in api_server, discord, and SSRF-guard tests, unrelated to this patch. Test count goes 3980 → 3991, exactly the 11 added here.

buzz-cli rejects an entire send when @name text does not match a current
channel member:

    user_error: mention '@-mention' does not match a current channel
    member; retry with --mention <pubkey> (exit 1)

The adapter never passed --mention, so any agent-authored message naming
another participant failed outright — including the plain-text fallback,
leaving the message undelivered. This blocks agent-to-agent coordination
in shared channels, where addressing another agent by name is the normal
interaction.

Resolve @name tokens against the channel roster and pass the matched
identities as repeated --mention arguments. Per the CLI contract,
supplying any explicit identity also downgrades unresolved or ambiguous
names to presentation-only, so partial resolution still averts a hard
failure.

Notes:
- `channels members` returns only {pubkey, role}, so display names come
  from the existing cached _resolve_user_name profile lookup.
- Ambiguous names (two members sharing a label) are left unresolved
  rather than notifying the wrong member.
- Raw hex and npub mentions bypass the roster lookup; self-mentions are
  dropped.
- Best-effort: any CLI or parse failure returns no mentions and leaves
  send behaviour exactly as before.
- Text without '@' skips the roster round-trip entirely.

Applied to send(), send_image() captions, and the standalone cron
delivery path via one shared helper.

Copy link
Copy Markdown
Contributor

I tested head 41e27df24a693cbc74f610affc7245bdd060379e against the exact PR base, today's main, and a live remote gateway.

Verified:

  • Focused adapter and WebSocket suite: 38 passed, 0 failed. Ruff is clean.
  • Full tests/gateway/ suite: 4,427 passed. The same 3 pre-existing baseline failures remain: two stale dated health fixtures and the Linux abstract AF_UNIX systemd test on macOS.
  • Current-main integration with fix(buzz): publish presence and preserve DM reply topology #74507, fix(buzz): discover newly joined channels dynamically #74823, and this PR: 62 focused tests passed after a semantic conflict resolution in adapter.py.
  • Direct live adapter test: the old path was rejected; a unique member name now sends, renders the mention, and produces the expected recipient p tag.
  • Full Fly-hosted Hermes gateway test: a new private channel was discovered without restart, Hermes replied, the reply referenced the owner, and the signed event contained the owner's p tag. The gateway PID did not change during the test. The temporary channel was deleted.

One correction is needed before merge. The PR's unknown-only behavior is not true against the current Buzz CLI. If a message contains only @Ghost or only an ambiguous name, _resolve_mention_pubkeys() returns an empty list. Buzz then has no explicit --mention and still rejects the send. I reproduced this live. The new scripted unit test passes because its mock accepts every messages send call and does not model Buzz's validation.

Recommended changes:

  1. Narrow the behavior table and test to the supported unique-member and raw-identity paths, or implement a safe unknown-only strategy without inventing a recipient.
  2. Make the unknown-only test model the CLI rejection so it cannot pass vacuously.
  3. Rebase after fix(buzz): publish presence and preserve DM reply topology #74507. The conflict is limited to the two send sites. The combined form should add repeated --mention arguments and retain fix(buzz): publish presence and preserve DM reply topology #74507's _reply_target handling.

The core member path is now "Signed, Sealed, Delivered." The unknown-only claim is the remaining blocker.

@alt-glitch alt-glitch added type/bug Something isn't working 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 30, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #73610 is the merged Buzz adapter foundation; this follow-up adds outbound mention identity resolution.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for covering the normal adapter, image-caption, and cron send surfaces; current main does construct Buzz messages send calls without --mention (plugins/platforms/buzz/adapter.py:587, 655, 1364), so the core premise is present.

Problems

  • Unknown-only/ambiguous-only messages are still sent with no explicit identity: the helper returns [] for no unique match (plugins/platforms/buzz/adapter.py:277-285 in this PR), and send() only appends flags from that list (line 668). The added unknown-name test (tests/gateway/test_buzz_adapter.py:493-498) uses a mock that pre-accepts every messages send call (line 454), so it does not model the reported CLI rejection.
  • The standalone path calls the helper without name_of (plugins/platforms/buzz/adapter.py:1468). Since the helper requires that callback for pubkey-only roster records (lines 263-273), cron delivery cannot resolve display names despite the stated coverage.

Suggested changes

  • Model the rejection when no --mention is present and either implement a safe unknown-only behavior or narrow that claim.
  • Add a standalone profile lookup plus a regression test for a pubkey-only roster.

Automated hermes-sweeper review.

assert sorted(_mentions_in(adapter._run_cli)) == sorted([HONEY_PUBKEY, FIZZ_PUBKEY])

@pytest.mark.asyncio
async def test_unknown_name_sends_without_mention(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test scripts messages send as accepted regardless of argv, so it cannot establish the claimed unknown-only behavior. Please make the fake reject unresolved @Name content when no --mention is present, then assert the intended supported outcome.

cli_path, cli_args, relay_url=relay, private_key=private_key
)

for pubkey in await _resolve_mention_pubkeys(message, target, _members_cli):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This call omits name_of. Because the helper only gets labels from inline member fields or that callback, a real {pubkey, role} roster cannot resolve @Name for standalone cron sends. Add a users get --pubkey resolver here or remove the standalone-name-resolution claim.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 30, 2026
@fraserpllc fraserpllc closed this Aug 8, 2026
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:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants