Repository navigation
Description
Summary
handle_join_request (kind:9021) hard-codes MemberRole::Member. An agent that joins a channel itself is therefore recorded as a human member — and because clients use the channel role as the "is this an agent" signal, that agent becomes invisible to @mention autocomplete, with no in-app way to repair it.
The same channel reached through a different entry point works fine, which is what makes this hard to spot.
Three entry paths, three different role decisions
| Entry path | How the role is decided |
|---|---|
| Desktop invite | role: user.isAgent ? "bot" : "member" — desktop/src/features/channels/ui/MembersSidebar.tsx |
kind:9000 / add-member |
handle_put_user reads a role tag; falls back to Member only for a brand-new member |
kind:9021 / self-join |
hard-coded MemberRole::Member; the joiner cannot express a role |
kind:9021 carries no role tag and buzz channels join exposes no --role, so this is not a client omission — there is no way to ask for anything else.
Why Member is the wrong default here specifically
MemberRole's own documentation says Bot is not a lower rung:
// crates/buzz-core/src/channel.rs
/// The hierarchy for permission checks is: Owner > Admin > Member > Guest.
/// Bot is a **separate designation** — it is not part of the linear hierarchy.
So assigning Member to an agent is not a conservative privilege choice — it is a category error, and it is the category clients read.
Impact
- Agent self-joins an open channel (the only channels where
9021is accepted). - It lands as
member. - Desktop's mention eligibility requires
isAgent === true || member.role === "bot", so it never appears in@autocomplete. - Repair requires
role: boton akind:9000, which the relay restricts to owner/admin (only owners/admins may change an active member's role) — and the Desktop role menu offers only admin/member/guest, and hides itself entirely for members already markedbot. There is no in-app path back.
Observed on a self-hosted relay: an agent self-joined at T, was unmentionable for the entire session, and only recovered after an owner-signed add-member --role bot.
Suggested direction
The relay already defines an agent discriminator and relies on it elsewhere:
// crates/buzz-db/src/usage.rs
/// Agent discriminator: `agent_owner_pubkey IS NOT NULL`.
Using that same discriminator in handle_join_request fixes the self-join path without touching handle_put_user, so explicit admin/member/guest assignment for humans is unaffected. PR to follow.
Alternatives, if maintainers prefer a different shape: accept an optional role tag on kind:9021, or have clients repair the role after joining. Both seem worse — the first lets a joiner self-assign, the second spreads the decision across clients that already disagree.
Note on closed relays
agent_owner_pubkey is exactly the field that stays NULL on relays with require_relay_membership = true (#5581). Where that is unresolved, the discriminator reports every agent as a human and this fix is a no-op — so the two are the same knot, and #5581 is a prerequisite for closed deployments rather than a separate concern.
Activity
rmichelena commentedon Nov 9, 2026
A fourth entry path with the same outcome: adding an agent from the mobile app records it as
member.addMembersis called without a role (add_members_sheet.dart:186), and the role defaults to'member'(channel_management_actions.dart:172)The picker in that sheet already knows the candidate is an agent (
isAgent = member.isBot || ownerPubkey != null,mention_candidates.dart:102), and Desktop's equivalent passesrole: user.isAgent ? "bot" : "member"(MembersSidebar.tsx:596).Observed on our relay today (2026-11-09). In the same channel, four agents were added within 21 minutes:
role bot, and all three eventually appeared in the macOS mention picker. One of them showed up only after a delay we have not explained yet.role member, and it never appears in the macOS picker, across two app restarts.The receiving side is the same as in this issue: Desktop only seeds relay-agent candidates from
bot-role membership unless the agent has an owner-authored kind:30177 (relay_directory.rs:156,agent_directory.rs:225). An agent recorded asmemberorowneris still classified as an agent elsewhere, so it is hidden rather than offered as a person. I'll open a small mobile PR that passesbotfor agent candidates. The broader fix is for Desktop to accept the same NIP-OA evidence that mobile already uses, which would cover self-join (this issue), mobile adds, and agent-created channels (#7161) together.