Repository navigation
Description
Summary
On a second device belonging to the same owner, the @ mention picker offers a persona as mintable even when an instance of that persona is already a member of the channel and running elsewhere. Selecting it creates a brand-new agent identity (new keypair, new kind:30177) instead of p-tagging the existing one.
The information needed to prevent this is already on the wire — ManagedAgentEventContent.persona_id — but the receiving device discards the whole event because it has no local record to merge into, so it never learns the pubkey → persona_id mapping that the picker's dedup depends on.
Line numbers below are from v0.5.0.
Repro
- Mac A: create persona Penelope, mint an instance, add it to
#channel. Works. - Mac B: same owner account, app freshly launched. The persona definition (
kind:30175) syncs and auto-inserts. No local instance is created (by design). - Mac B: in
#channel, type@Pen…. The picker shows two entries both labeled "Penelope" — the channel member, and a mintable persona. - Selecting the persona entry mints a second identity for the same persona and attaches it to the channel.
Now every @Penelope is a coin flip between two identities, and messages addressed to the one whose harness lives on the other machine are silently dropped.
Root cause
agentIdentityKey in desktop/src/features/agents/lib/agentAutocompleteEligibility.ts:114 is what should collapse these two candidates:
if (candidate.personaId) {
return `persona:${candidate.personaId}`;
}and agentCandidateRank (same file, :158) already ranks isMember first, so on a merge the live channel member would correctly win over the mintable persona.
The merge never happens because the member candidate has no personaId on device B. desktop/src/features/messages/lib/useMentions.ts:316:
personaId: managedAgentPersonaIdsByPubkey.get(pubkey) ?? linkedPersonaId,managedAgentPersonaIdsByPubkey is derived from the local managed-agent table, which on device B has no row for that pubkey. linkedPersonaId only resolves when a persona id happens to equal the agent pubkey, which is never true for minted instances. So the member candidate keys on label instead, the persona candidate keys on persona:<uuid>, and both survive into the list.
The persona is offered as mintable because the same local table gates it (useMentions.ts:389):
const personaCandidates = activePersonas
.filter((persona) => !managedAgentPersonaIds.has(persona.id))and selecting it routes to createPersonaAgentMutation in desktop/src/features/messages/ui/useMentionSendFlow.ts:354.
Why the local table is empty — and why that's the fixable part
apply_inbound_managed_agent in desktop/src-tauri/src/commands/personas/mod.rs:839 drops the inbound kind:30177 projection entirely when there is no local match:
/// No match is a no-op: managed agents carry device-local secrets and are never
/// minted from a relay event — an agent that does not already exist locally has
/// no secret key to run with, so inserting a secretless shell would be useless
/// and misleading. This diverges from the persona path, which DOES insert on no
/// match (personas are secretless definitions).
The security reasoning is right and shouldn't change: device B must not fabricate a runnable record it has no key for. But the projection it throws away contains persona_id, which is not a secret and is exactly the join key the picker needs. Dropping the event is stricter than dropping the secrets.
Impact
- Duplicate identities accumulate silently; each
@mentionfrom the non-minting device creates another. - Mentions become nondeterministic once duplicates exist.
- Messages to the identity whose harness lives on the other machine are dropped with no error (correctly —
useMentionSendFlow.ts:260skips non-local agents — but the user sees only silence). - Related second-order failure: if device B has a stale local record (from an earlier accidental mint) whose harness was since deleted, the mention resolves to it and device B attempts a local
startAgentMutation(useMentionSendFlow.ts:265), producingcannot spawn agent <pubkey>: harness "<name>" was deleted.
Suggested fix
Keep the no-mint rule; record the association only.
On no-match in apply_inbound_managed_agent, persist the (pubkey, persona_id, name) triple into a separate non-runnable index rather than into managed-agents.json. Then source managedAgentPersonaIdsByPubkey from local records ∪ that index. The existing agentIdentityKey / agentCandidateRank logic then does the right thing with no further change: the two candidates collapse and the channel member wins.
A narrower alternative, if a new store is unwelcome: have the relay agent directory (relayAgentsQuery) carry persona_id, so useMentions.ts:341 and :316 can resolve it without any local state. That fixes the picker but leaves other cross-device persona↔instance lookups unaware.
Either way the desired end state is the one the code already reaches for on a single device — a persona with a live instance anywhere in the channel is tagged, not re-minted.
Environment
- Buzz desktop v0.5.0, two macOS machines, one owner account
- Self-hosted relay (
ghcr.io/block/buzz), agent runs under a custom harness on machine A
Activity
Czaruno commentedon Jul 30, 2026
Reproduced on 0.5.2 in about 69 seconds. Relay-side event trace below, plus a negative control from the same build.
Setup: self-hosted relay, Buzz Desktop 0.5.2, one account signed in on two installs. All events read directly from the relay Postgres rather than from a client view.
Steps:
Persona
f114d13bended up with two keys 69 seconds apart:Four event kinds in the same second. The relay
userstable went from 15 rows to 17, with two rows carrying an identical display name and an identical avatar URL.The minted key is the one that stays live here. Key one has a single event in its whole life, the profile at 00:10:23. Key two has six events over the next 38 seconds and holds the channel membership. In an earlier occurrence on 0.5.1 the original stayed live and the duplicate went dormant, so which key survives appears to vary.
Negative control, same build, 118 seconds earlier. A different agent whose key was present locally was added to the same channel from the install that owned it. The relay recorded only kind 40099 and kind 44100, plus a reaction signed by that agent's original key. No kind 0, no kind 30177, no new key, counts unchanged. A channel add by itself does not mint. The missing local key is the necessary condition.
Not verified from the relay: which install performed which action. The events carry no install identifier, so the two-install sequence above is the operator's account of what they did and not something the event log proves.
Two earlier duplicates on this account show the same signature, both on 0.5.1: 07-28 21:51:36Z and 07-29 01:25:13Z.
marcbal77 commentedon Aug 9, 2026
Confirmed on Buzz Desktop 0.5.8 (macOS) in a same-device managed→native gateway migration, not only the second-device case described here.
Variant reproduced
9024599e…1b4c, display nameTheo) was already arole=botmember of a private Stream channel and was actively served by an external/native Hermes gateway.@Theofrom the composer caused Buzz to mint a fresh managed identity (f41811fe…bd61) from that persona instead of targeting the existing channel member.managed-agents.jsongained the fresh pubkey at the exact send timestamp, and a newbuzz-acpprocess started for it.Relay checks before and after showed canonical
9024599e…1b4cremained a channel member. This supports the issue's root-cause description: the picker does not reliably reconcile the persona definition with an existing member instance when no local runnable record links them.One additional UI problem: the later lifecycle event “Theo left the channel” used only the shared display name/avatar, making the accidental identity's lifecycle appear to apply to canonical Theo. Showing a pubkey prefix or otherwise disambiguating same-name identities in these system events would prevent this confusion.
No private keys or auth tags are included here. Reproduced and relay-verified by Theo on behalf of the affected user.