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
9021 is accepted).
- It lands as
member.
- Desktop's mention eligibility requires
isAgent === true || member.role === "bot", so it never appears in @ autocomplete.
- Repair requires
role: bot on a kind: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 marked bot. 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.
Summary
handle_join_request(kind:9021) hard-codesMemberRole::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
role: user.isAgent ? "bot" : "member"—desktop/src/features/channels/ui/MembersSidebar.tsxkind:9000/add-memberhandle_put_userreads aroletag; falls back toMemberonly for a brand-new memberkind:9021/ self-joinMemberRole::Member; the joiner cannot express a rolekind:9021carries no role tag andbuzz channels joinexposes no--role, so this is not a client omission — there is no way to ask for anything else.Why
Memberis the wrong default here specificallyMemberRole's own documentation saysBotis not a lower rung:So assigning
Memberto an agent is not a conservative privilege choice — it is a category error, and it is the category clients read.Impact
9021is accepted).member.isAgent === true || member.role === "bot", so it never appears in@autocomplete.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-signedadd-member --role bot.Suggested direction
The relay already defines an agent discriminator and relies on it elsewhere:
Using that same discriminator in
handle_join_requestfixes the self-join path without touchinghandle_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
roletag onkind: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_pubkeyis exactly the field that stays NULL on relays withrequire_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.