Skip to content

External (non-desktop-managed) agents: publishing kind:10100 makes them un-mentionable — shouldHideAgentFromMentions's invocability logic is unreachable #2987

Description

@ilporyynanen-dev

Component: desktop
Version: v0.4.26, source at 0096d710ed2e6abab19aaf7cdc14e3ee603d7ec8

Summary

We run two headless buzz-acp agents on a server (systemd, one per team member, each on its own subscription). They authenticate via NIP-42, are relay members, join open channels via kind:9021, and
respond correctly — the server side works great.

The problem is desktop UI eligibility: for an agent the desktop does not manage, publishing a kind:10100 agent profile makes the agent disappear from @-mention autocomplete (and from the
add-member search), even when the 10100 content explicitly declares the current user invocable via respond_to: "allowlist" + respond_to_allowlist. This forces an either/or choice for external
agents:

  • No kind:10100 → agent is mentionable (appears as a plain member), but is absent from the Agents directory.
  • kind:10100 published → agent appears in the Agents directory, but can no longer be @-mentioned from the composer at all — and since buzz-acp subscribes to mentions, the agent becomes
    effectively unusable in channels.

Root cause

useMentions.ts:249 runs the managed-list gate before the invocability logic:

if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) {
  return;                       // ← any isAgent=true candidate not managed by THIS desktop is dropped here
}
if (shouldHideAgentFromMentions({ ... })) { return; }

isAgentIdentityInManagedList (agentAutocompleteEligibility.ts:57) returns false for every isAgent === true candidate whose pubkey is not in the local managed list — so the candidate never reaches
shouldHideAgentFromMentions.

But shouldHideAgentFromMentions (same file, line 67) contains carefully-written logic for exactly this case ("Option B"): show a member agent unless the relay directory gives an explicit
not-invocable signal, backed by relayAgentIsSharedWithUser, which checks respond_to === "allowlist" && respond_to_allowlist.includes(currentPubkey). Because of the earlier gate, this branch is
unreachable for every non-managed agent
— the only candidates that survive the gate are managed agents, which are already unconditionally mentionable via mentionableAgentPubkeys. The invocability
check appears to be dead code, which suggests the gate ordering broke an intended behavior rather than implementing one.

The same pattern exists in the channel members add-search (MembersSidebar.tsx:285), so a non-managed agent with a 10100 profile cannot be added to channels from the UI either.

Reproduction

  1. Create a headless agent: relay member, NIP-42 auth, buzz-acp with --respond-to=allowlist including user A's pubkey; join an open channel (kind:9021).
  2. No kind:10100 published: user A types @ in that channel → agent appears (as a regular member); mention works; agent responds. ✓
  3. Publish kind:10100 for the agent, content:
    {"name":"Agent","agent_type":"agent","respond_to":"allowlist","respond_to_allowlist":["<user A pubkey hex>"]}
  4. After the directory refetch (≤5 min or app restart): @ no longer offers the agent to user A. Deleting the 10100 (kind:5, e-tag) restores mentionability.

Verified the relay serves the correct, latest-only 10100 with the allowlist fields; the raw→camelCase mapping in fromRawRelayAgent is correct; the drop happens purely in the gate above.

Expected

A directory agent whose kind:10100 declares the current user invocable (respond_to: "anyone", or allowlist including the user) should be mentionable and addable, per the logic already present in
shouldHideAgentFromMentions / relayAgentIsSharedWithUser.

Suggested fix

Let candidates that pass relayAgentIsSharedWithUser through the managed-list gate, e.g.:

if (
  !isAgentIdentityInManagedList(candidate, managedAgentPubkeys) &&
  !mentionableAgentPubkeys.has(normalizePubkey(candidate.pubkey))
) {                                                                       
return;
} 

(and the analogous change in MembersSidebar.tsx), which makes the existing Option B logic reachable while preserving the current behavior for unmanaged agents with no invocability signal.

Side notes from the same investigation (happy to split into separate issues)

  • kind:5 a-tag deletion silently no-ops for plain replaceable kinds (e.g. 10100:<pubkey>:): handle_a_tag_deletion only applies coordinates for parameterized-replaceable (30000-range) kinds. The
    deletion event is accepted and stored, but the target keeps being served — surprising combination. An e-tag deletion of the latest version works.
  • buzz-acp never publishes kind:0 or kind:10100 for its identity, so headless agents show as bare npubs until profiles are published out-of-band (we used buzz users set-profile + a small NIP-42
    publishing script). A --publish-profile option on buzz-acp would make headless deployments much smoother.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions