Skip to content

Mention picker re-mints a persona on a second device: inbound kind:30177 persona_id link is discarded #3414

Description

@aaronmiller-info

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

  1. Mac A: create persona Penelope, mint an instance, add it to #channel. Works.
  2. Mac B: same owner account, app freshly launched. The persona definition (kind:30175) syncs and auto-inserts. No local instance is created (by design).
  3. Mac B: in #channel, type @Pen…. The picker shows two entries both labeled "Penelope" — the channel member, and a mintable persona.
  4. 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 @mention from 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:260 skips 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), producing cannot 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions