Skip to content

fix(desktop): let channel-member agents be mentioned - #5

Merged
yjc801 merged 2 commits into
mainfrom
claude/agent-mention-member-gate
Aug 9, 2026
Merged

yjc801 merged 2 commits into
mainfrom
claude/agent-mention-member-gate

Conversation

@yjc801

@yjc801 yjc801 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

An agent owned by someone else could never be mentioned, even when its owner had explicitly allowlisted the mentioning user. Found while investigating why an agent that was a bot-role member of a channel, with permission granted, simply did not appear in the @ picker.

The mention picker ran two agent gates in sequence, and the first made the second's intent unreachable:

if (!isAgentIdentityInAllowedList(candidate, mentionableAgentPubkeys)) return;
if (shouldHideAgentFromMentions({ ... })) return;

isAgentIdentityInAllowedList rejects every candidate flagged isAgent that isn't already in mentionableAgentPubkeys. shouldHideAgentFromMentions is a strict superset of that check except for exactly one case, which it handles deliberately and documents at length:

// Member (Option B): hide only when we have an explicit not-invocable
// signal — a relay directory (kind:10100) entry that excludes us.
// Unknown invocability (not in directory) => show.

That branch never ran. The first gate dropped the candidate before it could apply.

Why the effect is total, not marginal. A relay agent enters mentionableAgentPubkeys only via channel_ids / respond_to / respond_to_allowlist, read from its kind:10100 profile. Nothing in this repo publishes that event. The only writer is buzz channels set-add-policy, whose content is just {"channel_add_policy": …} — and since kind 10100 is replaceable (asserted at compile time in buzz-core/src/kind.rs), that write also erases any profile that did exist. So the directory is empty in practice, no other-owner agent can enter the allowed set, and all of them were hidden.

The fix: gate on shouldHideAgentFromMentions alone.

Reviewer notes

  • Nothing else loosens. Non-member agents are still hidden (if (!isMember) return true), and a directory entry that excludes the viewer still hides (return directoryAgentPubkeys.has(...)). The only behavior change is the case the hide rule always intended to allow.
  • MembersSidebar deliberately keeps the stricter predicate. Deciding whether to add an agent to a channel is a different question from whether to mention one already in it.
  • The new test pins the disagreement between the two predicates on that single input, so the gate isn't reinstated by someone reading isAgentIdentityInAllowedList as the obvious guard.
  • Honest limitation: I could not write a test that fails if the gate is re-added at the call site. useMentions is a hook with a dozen queries and the repo has no hook-testing harness, so the protection here is a pinned-contract test plus a comment at the call site — documentation-grade, not enforcement.
  • Out of scope, filed separately: that set-add-policy erases agent profiles, and that kind:10100 has no producer at all, are both upstream (block/buzz) problems. This PR only stops the desktop from hiding agents because of them.

Related issue

None found (fork).

Testing

  • pnpm test: 4553 passed (1 new — asserts shouldHideAgentFromMentions shows a channel-member agent with no directory entry, while isAgentIdentityInAllowedList rejects the same input).
  • pnpm exec tsc --noEmit: clean.
  • pnpm check (biome + file-size + px-text + pubkey-truncation): clean, only the two known pre-existing infos.
  • Verified live. Built a release bundle from this branch (--no-sign) and ran it against a real relay: an agent owned by another user, holding bot role in the channel and permitted by its owner, now appears in the @ picker. It was absent on main under identical conditions.

No screenshots — the change is the presence of a row in an autocomplete that requires a specific multi-user setup to reproduce.

🤖 Generated with Claude Code

yjc801 and others added 2 commits August 8, 2026 10:24
The mention picker ran two agent gates in sequence, and the first made
the second's intent unreachable:

    if (!isAgentIdentityInAllowedList(candidate, mentionable)) return;
    if (shouldHideAgentFromMentions({...})) return;

`isAgentIdentityInAllowedList` rejects every candidate flagged `isAgent`
that is not already in `mentionableAgentPubkeys`. `shouldHideAgentFromMentions`
is a strict superset of that check except for exactly one case, which it
handles deliberately and documents at length: a channel-member agent with
no kind:10100 directory entry is *shown*, because unknown invocability is
not evidence of exclusion.

That branch never ran. Any agent owned by someone else was dropped by the
first gate before it could apply.

The practical effect is total: `mentionableAgentPubkeys` admits a relay
agent only via `channel_ids` / `respond_to` / `respond_to_allowlist` read
from its kind:10100 profile — and nothing in this repo publishes that
event. The only writer is `buzz channels set-add-policy`, whose content is
just `{"channel_add_policy": …}`; since kind 10100 is replaceable, that
write also erases any profile that did exist. So the directory is empty in
practice, no other-owner agent can enter the allowed set, and every one of
them was hidden — even when its owner had explicitly allowlisted the
mentioning user.

Gate on `shouldHideAgentFromMentions` alone. Non-member agents are still
hidden, and a directory entry that excludes the viewer still hides
(`return directoryAgentPubkeys.has(...)`), so the only behavior change is
the case the hide rule always meant to allow.

MembersSidebar keeps the stricter predicate — deciding whether to ADD an
agent to a channel is a different question from whether to mention one
already in it.

Verified: desktop tests 4553 passed (1 new, pinning the disagreement
between the two predicates so the gate is not reinstated), tsc clean,
pnpm check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>
Review finding (P2, useMentions.ts:257): this PR admits a channel-member
agent with no kind:10100 directory entry into the mention picker, but
`knownAgentPubkeys` stayed exactly `mentionableAgentPubkeys`, so
`isAgentPubkey` returned false for that same identity. After selection
the send flow therefore dropped it from `explicitAgentPubkeys` and
`preparedAgentPubkeys` — with "Keep addressed agents active" on, the
agent vanished from the composer after the first thread send and was
omitted from `sync_agents_to_active_huddle`.

The picker's member branch and the send path's classification are now
one gate: `getAdmittedMemberAgentPubkeys` lives beside
`shouldHideAgentFromMentions`, so the two cannot drift apart.

`useMentionAgentPubkeys` derives the three sets the mention surface
needs — invocable, member-agent, and known (invocable plus admitted
member agents) — and `memberAgentPubkeys` becomes the single source for
the candidate loop's `isAgent` instead of a repeated inline expression.
It is a separate module because `useMentions.ts` already sat at the
1000-line ratchet; the guard says split, not bump.

Replaces the smoke test that asserted this agent stays hidden. It passed
only by checking for zero rows before member and profile data settled,
so it could not see the regression. The new test drives picker selection
through a thread send and asserts the mention is retained, promoted into
the thread's stored audience, and enrolled in the Huddle sync; it fails
on the pre-fix classification with an empty composer.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
@yjc801
yjc801 merged commit 50dce49 into main Aug 9, 2026
41 of 47 checks passed
@yjc801
yjc801 deleted the claude/agent-mention-member-gate branch August 9, 2026 02:58
yjc801 added a commit that referenced this pull request Aug 21, 2026
Resolves the desktop agent-mention conflicts from upstream block#6338
("Fix cross-owner relay agent mentions in owner-only builds"), which
removed the owner-only gate from mention admission that this fork had
also been carrying.

- agentAutocompleteEligibility: take upstream's removal of the
  ownerOnly/isManagedAgent/ownerPubkey gate; keep the fork's
  lenient channel-member branch (#5) and its directoryAgentPubkeys
  input, which upstream's change does not cover.
- agentMentionRevalidation: same — drop the owner-profile proof fetch,
  keep the roster fetch that the member branch depends on.
- useMentions: keep the fork's per-community relay URL (#4), drop the
  owner-only query.
- MembersSidebar: keep the fork's MembersSidebarAddMemberRows split and
  delete upstream's duplicate AddMemberSearchResultRow.tsx.
- relayReconnectReplay.test.mjs: upstream's new coupling guard reads the
  drift literal from ingest.rs; this fork hoists it into buzz-core for
  buzz-waker, so point the guard at the real definition.

Validated: desktop tsc, 5438 desktop tests, biome, px/file-size guards,
cargo fmt, cargo check (workspace + Tauri, all targets), 2781 Tauri lib
tests.

Signed-off-by: Junchao Yan <yjc801@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant