Desktop: a second install of the same identity cannot address agents at all — empty mention picker, hidden members, and typed mentions silently drop the p tag (mobile unaffected)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
Buzz Desktop decides which agents you can interact with from **what is registered on that machine**, not from what the relay says. A second desktop signed in as the same identity, in the same community, sees **zero** agents — no `@` autocomplete, no member-list entries, no ability to start a DM — even though those agents are `role=bot` members of the channels it is displaying and are actively posting in them.
**This is not just a missing autocomplete.** The same filtered candidate list is what resolves mentions at send time, so typing an agent's name by hand does not work either: the message sends, renders with what looks like a mention, and carries **no `p` tag**, so the agent is never notified and never replies. There is no workaround inside a channel — the affected install simply cannot address an agent.
The mobile client does not have this limitation. It lists channel members directly, so the same account on a phone can mention the same agents without issue. The two clients implement the same feature with different rules, and mobile's implementation carries a comment claiming it mirrors desktop's.
## Impact
A desktop install that does not itself host the agent processes is effectively read-only with respect to agents, and fails **silently** — the message posts normally and looks correct. Nothing in the UI indicates that no one was addressed.
This bites in the ordinary two-machine case (a laptop and a desktop on one identity) and there is no setting that fixes it. The only in-app workaround is a DM conversation that already exists, because open conversation lists are not filtered. New DMs cannot be created, since the recipient search applies the same gate.
Registering the agents locally on the second machine is not a workaround: creating an agent record mints a **new keypair**, producing a duplicate identity that is a different agent as far as the relay is concerned.
## Root cause
Every agent candidate on desktop must pass `isAgentIdentityInAllowedList` against `mentionableAgentPubkeys`. Critically this gate is applied inside `addCandidate`, so it filters the **candidate list itself** — not merely the dropdown:
`desktop/src/features/messages/lib/useMentions.ts:250-271`
```ts
const mentionCandidates = React.useMemo(() => {
const candidatesByPubkey = new Map();
const addCandidate = (candidate: MentionCandidate & { pubkey: string }) => {
const pubkey = normalizePubkey(candidate.pubkey);
if (isArchivedDiscovery(pubkey)) return;
if (!isAgentIdentityInAllowedList(candidate, mentionableAgentPubkeys)) return;
...
```
That set is built from exactly two sources — `desktop/src/features/agents/lib/agentAutocompleteEligibility.ts:47-82`:
1. `managedAgentPubkeys` — agents registered locally on this machine
2. relay agents holding a **kind:10100** record whose `channel_ids` includes the channel and whose `respond_to` permits the current user
**Source 2 is empty in practice, because nothing publishes a kind:10100 agent directory record.** Grepping every `KIND_AGENT_PROFILE` write site:
- `desktop/src-tauri/src/relay.rs:444-450` — `sync_managed_agent_profile` builds a **kind:0** event (display name and avatar only)
- `crates/buzz-cli/src/commands/channels.rs:1004-1046` — `buzz channels set-add-policy`, the only kind:10100 writer, publishes `{"channel_add_policy": ""}` with no `name`, `respond_to`, or `channel_ids`
Since kind:10100 is replaceable, `set-add-policy` would in fact *overwrite* a richer record with a minimal one.
With source 2 empty, the rule reduces to: **you can only address agents that run on the machine you are typing on.**
### Why typing the name by hand also fails
`extractMentionPubkeys` is what populates the outgoing event's `p` tags, and it resolves names against `mentionCandidates` — the list the gate above already emptied:
`desktop/src/features/messages/lib/useMentions.ts:794-829`
```ts
for (const candidate of mentionCandidates) {
if (!candidate.pubkey) continue;
if (!candidate.isMember) continue;
...
if (name && hasMention(text, name)) {
pubkeys.push(candidate.pubkey);
}
}
```
The other source it consults, `mentionMapRef`, is populated only when a picker item is *selected* — which is impossible when the picker is empty. So on the affected machine both resolution paths are dead and the `p` tag is never emitted.
### Surfaces affected
| Surface | File | Line |
|---|---|---|
| `@` mention autocomplete | `desktop/src/features/messages/lib/useMentions.ts` | 258 |
| Mention resolution at send time (`p` tags) | `desktop/src/features/messages/lib/useMentions.ts` | 794-829 |
| Channel members sidebar | `desktop/src/features/channels/ui/MembersSidebar.tsx` | 297 |
| New-DM recipient search | `desktop/src/features/messages/ui/useNewMessageRecipients.ts` | 131-132 |
### DMs use a different scope
`useMentions.ts:197-206` uses channel scope only when the channel type is `stream` or `forum`; DMs get `{ type: "managed-only" }`, which rejects relay agents unconditionally. So even if kind:10100 records were published, they could never fix mentions inside a DM.
### Mobile does it differently
`mobile/lib/features/channels/mentions/mention_candidates.dart:59-79` adds every channel member unconditionally — no allowlist gate, no kind:10100 requirement:
```dart
for (final member in members) {
final pk = member.pubkey.toLowerCase();
if (!seen.add(pk)) continue;
final profile = userCache[pk];
final ownerPubkey = ownerByAgentPubkey[pk] ?? profile?.ownerPubkey;
final isAgent = member.isBot || ownerPubkey != null;
candidates.add(MentionCandidate(... isMember: true, role: member.role));
}
```
Mobile applies an invocability check only to **non-member** agents, and its comment at `mention_candidates.dart:118-121` states it "Mirrors desktop's `shouldHideAgentFromMentions` for non-member agents." Desktop applies its gate to members too, so the behaviours diverge for exactly the common case.
`mobile/lib/features/pulse/pulse_provider.dart:110-128` additionally falls back to the relay member list (`kind:13534`, `role=bot`) with the comment "This catches managed agents that may not have published a kind:10100 profile event yet" — acknowledging in-tree that 10100 records are frequently absent. Desktop has no equivalent fallback.
### Probable dead code
Because the `isAgentIdentityInAllowedList` gate at `useMentions.ts:258` already guarantees membership in `mentionableAgentPubkeys`, the subsequent `shouldHideAgentFromMentions` call can never return `true` for an agent — its first branch is `if (mentionableAgentPubkeys.has(normalized)) return false`. Its documented "member with unknown invocability => show" behaviour (`agentAutocompleteEligibility.ts:95-125`) is therefore unreachable. That comment suggests the intended behaviour is closer to mobile's, and the earlier hard gate defeats it.
## Steps to reproduce
1. Set up a community with an agent whose harness runs on machine A, and add it to a `stream` channel with `role=bot`
2. Install Buzz Desktop on machine B and sign in with the same identity
3. Open the shared channel on machine B — the agent's messages are visible, and it is a member
4. Type `@` in the composer → the agent is absent from autocomplete
5. Type the agent's display name manually and send
**Expected:** the agent appears in autocomplete, as it does on machine A and on mobile; and a typed mention resolves to a `p` tag.
**Actual:** the agent is absent from autocomplete, absent from the members sidebar, and cannot be selected as a DM recipient. The message in step 5 sends successfully with **no `p` tag**, so the agent is never triggered and never responds. No error or warning is shown.
Verified against the relay: the agent is still `role=bot` in the channel throughout, and the sent event carries only `[["h",""]]`.
## Suggested fix
Preferred — **align desktop with mobile**: treat channel membership as sufficient for an agent that is a member of the current channel, and reserve the invocability gate for non-member agents. This appears to be what `shouldHideAgentFromMentions` already intends; removing or narrowing the `isAgentIdentityInAllowedList` call at `useMentions.ts:258` would let that logic run. The same change is needed at `MembersSidebar.tsx:297` and `useNewMessageRecipients.ts:131-132`.
Alternative — **actually publish kind:10100 records**. If the directory is meant to be the source of truth, something has to write it. Today no code path does, so the field is load-bearing but never populated.
Independently of which is chosen: **a mention that fails to resolve should not send silently.** Warning the user that no recipient was addressed would have made this diagnosable in seconds rather than hours.
## Environment
- macOS 15.4 (Darwin 25.4.0), Apple silicon
- Buzz Desktop 0.5.5 (source read at `main` / `2b873cf`, version 0.5.6)
- Hosted community on `communities.buzz.xyz`
- Two desktops and one phone on a single identity; agents hosted on one desktop only
---
*Filed by an agent on behalf of the affected user, who owns the two-desktop environment described above. All code references were read at `2b873cf` and the relay-side behaviour was verified live.*
Contributor guide
Assessment
This issue has not been assessed yet.