Repository navigation
fix(desktop): let channel-member agents be mentioned - #5
Merged
Merged
Conversation
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 <[email protected]>
Signed-off-by: Junchao Yan <[email protected]>
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 <[email protected]>
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 <[email protected]>
yjc801
pushed a commit
that referenced
this pull request
Oct 28, 2026
…ction (block#8606) Follow-up to block#8575. ## Problem Today a kind:9000 is stored first and the membership write runs afterwards, on its own. If that write fails, the relay logs it and still returns an accepted OK. A resend of the same event then returns `duplicate:`, so the person is never added. This is the catch-log-and-accept failure that Review-Proven Rules #1 and #5 describe. ## Change kind:9000 now uses the same atomic path as kind:9002 (`persisted_atomically` in ingest). - **`buzz-db`:** new `put_members_with_event`, which runs one transaction: 1. Take the channel's membership lock, before the event insert, the TTL lock and the channel row. This matches the order the other membership writers use, so it adds no new lock cycle. 2. Store the event. If it's already stored, roll back and return `Duplicate`. 3. Run the existing `put_members` decisions and upsert. These now live in a shared `put_members_on` step on the transaction. 4. Commit. - **Failure:** nothing is stored and the client gets an `error:` rejection, so it can resend the same event and have it applied. - **Everyone refused:** if the decisions under the lock refuse every person, nothing is stored and the event is rejected with the same text validation gives. Before, a single-person add refused at that point was accepted with an empty message. - **After commit:** the live effects stay as they were. They're best-effort and read the committed state: cache invalidation, `member_joined` notices, member-added notifications and 39002 discovery. - **`NOSTR.md`:** now says a failed kind:9000 can be resent as is. ## What got simpler - kind:9000 no longer has its own store-then-apply path in ingest. It shares the atomic branch with 9002 and workflow deletion. - `apply_put_user`'s "write failed, report it as a per-person rejection inside an accepted OK" branch is gone. - The `Db::put_members` wrapper is deleted. Its only caller was the kind:9000 path. ## Identified debt (not in this PR) kind:9001 (remove), 9021 (join) and 9022 (leave) have the same gap. Each stores the event, then runs the membership write in `handle_side_effects`, and a failure is only logged. kind:9008 and 9005 follow the same store-then-apply pattern. These can move onto the same atomic shape in a follow-up. ## Tests - `failed_put_user_stores_nothing` (relay, Postgres): a test trigger fails the second person's row in a two-person add, after the event row and the first person were already written in the same transaction. It checks: - Both the send and a resend are rejected with `error:`. - No kind:9000 is stored, nobody is added, and no `member_joined` notice is posted. - With the trigger dropped, the same signed event adds both people. - One more resend returns `duplicate:` and posts no second notice. - `put_members_with_event_stores_the_event_with_its_writes` (buzz-db): checks the three outcomes: - Refused: nothing is stored. - Applied: the event is stored and the person is written. - Duplicate: the roster is left alone. - `put_user_refused_under_the_lock_stores_nothing` (relay, Postgres): a test trigger removes the sender from a private channel inside the write transaction, so validation passes and the decision under the lock refuses. For a single add and a batch, it checks the `invalid:` rejection, that nothing is stored and no notice is posted, and that the same event applies once the trigger is gone. - Mutation checks, run in Blox gates: - At `d18e88fd`: committing the refused event fails the buzz-db test; applying a duplicate fails the first two tests; storing the event in its own transaction fails the first two tests. - At `3b106623`: turning the refusal under the lock into `error:` fails the new refusal test, and so does committing the refused event. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
isAgentIdentityInAllowedListrejects every candidate flaggedisAgentthat isn't already inmentionableAgentPubkeys.shouldHideAgentFromMentionsis a strict superset of that check except for exactly one case, which it handles deliberately and documents at length: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
mentionableAgentPubkeysonly viachannel_ids/respond_to/respond_to_allowlist, read from its kind:10100 profile. Nothing in this repo publishes that event. The only writer isbuzz channels set-add-policy, whose content is just{"channel_add_policy": …}— and since kind 10100 is replaceable (asserted at compile time inbuzz-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
shouldHideAgentFromMentionsalone.Reviewer notes
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.MembersSidebardeliberately 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.isAgentIdentityInAllowedListas the obvious guard.useMentionsis 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.set-add-policyerases 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 — assertsshouldHideAgentFromMentionsshows a channel-member agent with no directory entry, whileisAgentIdentityInAllowedListrejects 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.--no-sign) and ran it against a real relay: an agent owned by another user, holdingbotrole in the channel and permitted by its owner, now appears in the@picker. It was absent onmainunder 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