From 5d497f185d9452446a616cd0f3b1f6dddc3ee94f Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 27 Jul 2026 19:42:52 -0400 Subject: [PATCH] fix(desktop): mention autocomplete gates on managed agents only, hiding every hosted agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isAgentIdentityInManagedList` is called with `managedAgentPubkeys` — agents this installation runs locally. Any other agent arrives with isAgent=true and is dropped there, BEFORE `shouldHideAgentFromMentions`, so `relayAgentIsSharedWithUser`, the respond_to allowlist and channel membership are never consulted for it. That makes an entire branch of the eligibility module unreachable: an agent hosted elsewhere cannot be @-mentioned no matter what it publishes, even when it is a member of the channel, is in the user's own respond_to allowlist, and appears in the relay directory — and the relay-agent candidate loop a few lines below, which exists to offer exactly those agents, can never produce a visible suggestion. The trigger is ordinary NIP-OA attestation: the relay sets users.agent_owner_pubkey (write-once), the members/profiles API returns is_agent, and the candidate is flagged an agent. So attesting an agent — the thing that earns it the bot badge and "managed by " — is also what makes it unmentionable. Reproduced against a hosted 8-agent fleet: all eight were channel members with the user in their allowlist and complete kind:10100 profiles, and none could be @-mentioned; three locally-managed agents in the same channel could. Gate on managed ∪ relay directory instead, via the same `mergeKnownAgentPubkeys` helper `useKnownAgentPubkeys` already uses app-wide, restoring the intended layering: identity/ownership decides who is KNOWN, the eligibility rules decide who is INVOCABLE. `shouldHideAgentFromMentions` still applies, so a directory agent that excludes this user is still hidden. --- .../src/features/messages/lib/useMentions.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts index 0c73b753390..72bcb5e1bc9 100644 --- a/desktop/src/features/messages/lib/useMentions.ts +++ b/desktop/src/features/messages/lib/useMentions.ts @@ -11,6 +11,7 @@ import { } from "@/features/channels/hooks"; import { useIsArchivedPredicate } from "@/features/identity-archive/hooks"; import type { MentionSuggestion } from "@/features/messages/ui/MentionAutocomplete"; +import { mergeKnownAgentPubkeys } from "@/features/agents/knownAgentPubkeys"; import { coalesceAgentAutocompleteCandidates, coalesceAutocompleteCandidatesByKey, @@ -221,6 +222,24 @@ export function useMentions( return lookup; }, [managedAgentsQuery.data, personasQuery.data]); const knownAgentPubkeys = mentionableAgentPubkeys; + // Managed agents ∪ relay directory. `isAgentIdentityInManagedList` gates the + // autocomplete on THIS set, and passing only the managed agents makes every + // remotely-hosted agent unmentionable: the relay flags an attested identity + // `is_agent` (users.agent_owner_pubkey, write-once), the candidate therefore + // arrives with isAgent=true, and the gate drops it before + // `shouldHideAgentFromMentions` — so `relayAgentIsSharedWithUser`, the + // allowlist and channel membership are never consulted for it. That made the + // whole relay-agent branch of the eligibility module unreachable, including + // the relay-agent candidate loop below. + // + // The union is the set this app already treats as "known agents" everywhere + // else (`useKnownAgentPubkeys`, same `mergeKnownAgentPubkeys` helper); using + // it here restores the intended layering: identity/ownership decides who is + // KNOWN, and the eligibility rules decide who is INVOCABLE. + const knownOrManagedAgentPubkeys = React.useMemo( + () => mergeKnownAgentPubkeys(managedAgentsQuery.data, relayAgentsQuery.data), + [managedAgentsQuery.data, relayAgentsQuery.data], + ); const activePersonas = React.useMemo( () => (personasQuery.data ?? []).filter((persona) => persona.isActive), [personasQuery.data], @@ -246,7 +265,7 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + if (!isAgentIdentityInManagedList(candidate, knownOrManagedAgentPubkeys)) { return; } if (