Skip to content

fix(desktop): refresh @ search when channel membership changes - #7191

Open
loganj wants to merge 2 commits into
fix/mention-picker-stack-844abd33-1from
fix/mention-picker-stack-844abd33-2
Open

fix(desktop): refresh @ search when channel membership changes#7191
loganj wants to merge 2 commits into
fix/mention-picker-stack-844abd33-1from
fix/mention-picker-stack-844abd33-2

Conversation

@loganj

@loganj loganj commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

Keeps the people and agents you can @ mention up to date as membership changes, without restarting or reopening anything.

  • Membership events refresh the shared directory. When someone is added to or removed from a channel — by you or anyone else — the directory behind @ search is marked stale immediately and refreshed, so the new member can be found with @ the next time you type it. Before, the list could stay stale until something else happened to reload it.
  • Bursts don't stampede. Several membership changes in quick succession coalesce into one refresh window instead of a pile-up of overlapping reads.
  • A stale read can't come back and win. A directory read already in flight when the membership change lands is cancelled, so an older result can never arrive late and overwrite the fresh one.
  • Switching communities starts clean. Queued refreshes are dropped when you switch community or signing identity, so work from the old community can't leak into the new one.

The refresh supplies up-to-date evidence for the picker only; it never grants permission or optimistically adds anyone to the list.

Part of eight mention-editor PRs: shared recovery prerequisite #7190 → {#7191#7192, #7196#7323#7197#7239#7240}. This PR builds on #7190 to preserve the baseline visible ambiguous-send failure behavior; membership freshness remains a separate feature unit.

Related issue

Continues the merged mention-editor work from #7124 (authorize remote mentions at publication) and #7128 (preserve spacing after multi-word mentions). No separate tracking issue for this slice.

Testing

  • New unit tests membershipDirectorySync.test.mjs cover immediate staleness, coalescing, duplicate-event suppression, cancellation of in-flight reads, and reset on community or signing-identity switch.
  • Rebased composition validation: TypeScript, whole-desktop Biome, repository file-size guard and E2E build pass. On the composed fix(desktop): show an agent in @ search after you add it to a channel #7192 tree, 88 focused scheduler/membership/send/helper tests and 11 accepted-write/roster edge probes pass; zero-retry browser checks pass for Create→Add→first @, baseline chat ambiguous-submit/error/draft/no-publication, and Welcome refreshed roster→preserved ambiguous negative→exact current starter sole-recipient completion. Unchanged freshness and restored Welcome source blobs were compared exactly with the prior reviewed source; no full-package rerun was needed for the mechanical replay. At the prior head ff2b68cb, CI run 34262722726 completed successfully, including Desktop Core, all four smoke shards, relay-backed integration, and macOS/Windows builds, with DCO, Semgrep and zizmor passing. Current head f66687ef adds the claim-1 fix — the app-wide background receiver now refreshes the mention directory on third-party membership rows — validated at this exact candidate by the full local desktop suite 6031/6031, focused red/green with deletion control, types and lint; its CI is dispatched and pending. This is lane-local evidence, not a ready-to-merge or all-lanes-green claim.

@loganj loganj changed the title fix(desktop): refresh agent discovery on live membership events fix(desktop): refresh @ search when channel membership changes Sep 2, 2026
@loganj
loganj force-pushed the fix/mention-picker-stack-844abd33-2 branch 3 times, most recently from 9164074 to b411da5 Compare September 4, 2026 11:17
@loganj
loganj force-pushed the fix/mention-picker-stack-844abd33-2 branch from b411da5 to 4316a13 Compare September 8, 2026 17:26
@loganj
loganj changed the base branch from fix/mention-picker-stack-844abd33-1 to integration/mention-picker-stack-844abd33-base September 8, 2026 17:27
@loganj
loganj force-pushed the fix/mention-picker-stack-844abd33-2 branch 2 times, most recently from e647e70 to 96c6d9c Compare September 8, 2026 17:36
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the fix/mention-picker-stack-844abd33-2 branch from 96c6d9c to ff2b68c Compare September 8, 2026 18:23
@loganj
loganj changed the base branch from integration/mention-picker-stack-844abd33-base to fix/mention-picker-stack-844abd33-1 September 8, 2026 18:23
@loganj
loganj marked this pull request as ready for review September 9, 2026 15:59
@loganj
loganj requested a review from a team as a code owner September 9, 2026 15:59

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES

Reviewed base bb8cca21e13099bc6206b18c62272b72928574ca through exact head ff2b68cb15bcd9d01ac37aedff5511b605166ad2.

1. Major — background third-party membership changes leave the mention directory stale

The global notification hook filters on the viewer's pubkey (desktop/src/features/channels/useMembershipNotifications.ts:52-60), while relay membership notifications p-tag only the changed member (crates/buzz-relay/src/handlers/side_effects.rs:913-928). Thus viewer V does not receive that path when actor A adds/removes target T. The mounted-channel path does refresh on membership system rows (desktop/src/features/messages/hooks.ts:375-390), but the all-channel subscription used for background channels accepts those rows without scheduling a refresh (desktop/src/features/channels/useLiveChannelUpdates.ts:232-335,385-401). V can therefore keep a removed agent visible, or omit a newly added agent, until polling despite already receiving the background-channel row.

Author action: detect membership rows in the all-channel live-event production path (or emit a genuinely viewer-addressed global event), invoke the coalescing scheduler there, and add a caller-bound regression for a background-channel A→T add/remove observed by V. Deleting the production invocation should make that test fail.

Verification owner: :bot: Jude’s code review agent will trace active/background delivery, run the full Desktop package, mutation-delete the invocation, and exercise V's first @ search after the background change.

2. Moderate — refresh/reorder can silently retarget keyboard selection

An open picker reranks when the directory changes (desktop/src/features/messages/lib/useMentions.ts:369-399), but selection is retained only as a numeric index and merely clamped (desktop/src/features/messages/lib/useMentionSelection.ts:7-23). Enter/Tab inserts whichever identity now occupies that index (useMentions.ts:776-815). A production-hook probe selected Bob in [Alice, Bob], then rerendered [Agent, Alice, Bob]; selection stayed at index 1 and therefore moved to Alice instead of following Bob to index 2. The next Enter/Tab can mention the wrong recipient without further navigation.

Author action: preserve selection by stable suggestion identity (normalized pubkey/persona ID/team ID) across refresh/reorder, with an explicit safe fallback if the identity disappears. Add a regression that changes ordering while the picker is open, asserts the visible highlight remains on the intended identity, and then verifies Enter/Tab inserts that identity. Restoring index-only clamping should fail the test.

Verification owner: :bot: Jude’s code review agent will verify keyboard/highlight/visible-row behavior and exact inserted recipient identity.

Evidence and confidence

  • Full Desktop package: 6,028 passed, 0 failed at the exact head; HEAD unchanged and review trees clean afterward.
  • git diff --check passed.
  • Exact-head CI run 34262722726: Desktop Core, four smoke shards, relay-backed Desktop E2E/integration, macOS/Windows builds green; DCO, Semgrep, and zizmor green. Cancelled entries belong to a superseded run, not this head.
  • Confidence gap, not a separate defect: no native GUI/accessibility journey was run. After the fixes, exact-head native evidence should cover a background third-party membership change and open-picker reorder.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent

Requesting changes on exact head ff2b68cb15bcd9d01ac37aedff5511b605166ad2 after reconciling both assigned lanes. Two independent author-actionable defects remain.

  1. Open-picker refresh can silently retarget keyboard selection. Membership refresh reranks suggestions (desktop/src/features/messages/lib/useMentions.ts:369-399), but selection persists only as a numeric index (desktop/src/features/messages/lib/useMentionSelection.ts:7-23); Enter/Tab inserts the identity now occupying that index (useMentions.ts:776-815). A production-hook probe selected Bob in [Alice, Bob], refreshed to [Agent, Alice, Bob], and retained index 1 (Alice) instead of Bob at index 2. This can mention the wrong recipient without further navigation.

    Required change: preserve selection by stable suggestion identity (normalized pubkey/persona/team identity) across reorder; use an explicit safe fallback if it disappears. Add a regression that reorders an open picker, asserts the same visible identity remains highlighted, then verifies Enter and Tab insert that identity.

  2. Third-party membership changes in background channels miss the refresh scheduler. The global notification hook filters #p to the viewer (desktop/src/features/channels/useMembershipNotifications.ts:52-60), but relay membership notifications tag the changed member (crates/buzz-relay/src/handlers/side_effects.rs:913-928). Mounted-channel rows refresh at desktop/src/features/messages/hooks.ts:375-390, while the all-channel background path receives system rows but never calls refreshDirectoryAfterMembershipChange (desktop/src/features/channels/useLiveChannelUpdates.ts:232-335,385-401). A viewer can therefore retain stale mention candidates after another actor changes a third party in a background channel.

    Required change: invoke the coalescing refresh scheduler from the all-channel production event path (or provide a viewer-addressed global event). Add a caller-bound behavioral regression for a background A→T add/remove observed by V; deleting the production invocation must fail it.

Baseline evidence was green: Desktop 6,028/6,028, clean git diff --check, and 17 successful/10 skipped exact-head checks. An older duplicate run is cancelled. Live head was unchanged immediately before review.

@loganj

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

On the open-picker reorder retargeting keyboard selection (Moderate finding) — valid at this PR's head; fix owned by #7197

Valid as reviewed at ff2b68cb: while the picker is open, selection persists only as a numeric index that is clamped on rerank (useMentionSelection.ts:7-23), and Enter/Tab then inserts suggestions[mentionSelectedIndex] — whichever identity occupies that index after the refresh (useMentions.ts:776-815). The probe's selected Bob silently becoming Alice in [Alice, Bob][Agent, Alice, Bob] is exactly this mechanism.

Per this stack's ownership split the fix is #7197's stable installed-snapshot chooser, not a backport here. At #7197's head 0464384b the picker installs a frozen row snapshot once per request (old?.request === request ? old : { request, rows: shallow-copy, index }useMentionSelection.ts:7-36), so background membership/directory/presence/ranking updates can neither replace nor reorder an installed list; that is the stated contract in docs/mention-editor.md ("Stable completion choices"). On top of that byte-identical chooser, #7239 only updates availability in place (useMentions.ts:479-500: identity, label and order stay frozen; only action/presence/Retry resolve live), and #7240's recent-choice history only ranks the next request (mentionSelectionHistory.ts) — never installed rows.

Existing coverage of this mechanism at #7239's head f1af1b88 (cited as written; not rerun for this comment):

Scope, stated plainly: standalone #7191 at ff2b68cb still carries the old moving-list model. This finding is closed at the combined stack state — the single #7197 request/snapshot owner (with #7239/#7240 layered on it) also carrying this PR's membership refresh — and nothing here has been verified on a combined head. Keeping one snapshot chooser instead of backporting a second copy into #7191 is the deliberate cross-PR resolution for this stack. The background third-party membership refresh finding is separate and remains open on this PR.

The app-wide background receiver merged kind 40099 channel-event rows
into the timeline cache but never refreshed the mention directory, so a
third-party member add/remove in a subscribed background channel left
mention candidates stale until the next focused directory poll. Parse
the member_joined/member_left/member_removed payload in
handleIncomingMessage and call the existing
refreshDirectoryAfterMembershipChange with the row's event id, mirroring
the mounted-channel path; the helper's coalesce, cancel and event-id
dedup semantics are unchanged. A mounted-caller regression drives the
real hook with relay-wire 40099 rows and asserts that non-membership
rows do not refresh.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

On the background third-party membership change leaving the mention directory stale (Major finding) — fixed at this PR's new head f66687ef, pending CI and re-review

Valid as reviewed at ff2b68cb (both review bodies, e.g. 5156991744): the app-wide background receiver — useLiveChannelUpdates' handleIncomingMessage, mounted via AppShell → useUnreadChannels — merged kind-40099 channel-event rows into the timeline cache but never refreshed the mention directory. The mounted-channel refresh only covers the channel on screen, and the global membership hook only sees viewer-addressed 44100/44101 rows, so a third-party add/remove in a subscribed background channel left candidates stale (or missing) until the focused directory poll.

Fix published as commit f66687ef989f14bfa026b16d4a35def9beb74b58, the new head of this PR: handleIncomingMessage now parses the member_joined/member_left/member_removed payload of kind-40099 rows and calls the existing refreshDirectoryAfterMembershipChange(queryClient, event.id) — the same one-liner the mounted-channel path makes, with the same payload gate and member set (useLiveChannelUpdates.ts:327-347, invocation at line 341). The helper's coalesce/cancel/event-id-dedup semantics are unchanged; no wire-format or relay change.

New mounted-caller regression, useLiveChannelUpdatesMembershipDirectory.test.mjs: mounts the real background receiver (createRoot/act inside a QueryClientProvider; relayClient subscription entry points mocked, no socket) and drives it with ordinary relay-wire kind-40099 rows — relay-keypair-signed, h-tagged, content {"type":…,"actor":A,"target":T} per side_effects.rs. The emit helper refuses to deliver a row the subscription filter would not carry, so the fixture cannot pass through a kind the hook never receives. The viewer subscribes to two channels and views the other one, so the membership channel is background.

Red / green / deletion control — actual runs bound to this exact candidate (the validated dirty state, now byte-identical to the published commit):

  • RED — pristine ff2b68cb handler + new test only: 2 fail / 1 pass, failing exactly at member_joined|member_removed row must invalidate the mention directory (false !== true); the negative control (non-membership 40099 topic_changed + plain stream message) passes unpatched.
  • GREEN — fix applied: 3/3 pass (add: directory invalidated synchronously, the 200 ms-coalesced refetch picks up the new roster; remove: roster drops the member).
  • Deletion control — deleting only the refreshDirectoryAfterMembershipChange(queryClient, event.id) invocation (detection block kept): 2 fail / 1 pass, the same wiring assertions; restoring the invocation returns to 3/3. The regression binds the production seam, not a test-only shortcut.
  • Combined focused suites across this production file and the helper it calls: 39/39 pass.

Full-suite proof scope: the entire desktop package suite was run once at this exact candidate — 6031/6031 tests pass, 87 suites, exit 0 (just desktop-test; this claim1 regression 3/3 and the helper's own coalesce/cancel/dedup suite 6/6 green inside it), plus desktop tsc --noEmit exit 0 and Biome clean on both changed files. These are local runs; remote CI is not yet asserted.

Stack note: the identical fix is mechanically replayed onto #7192's new head e7b8434 (range-diff content-identical, no other changes), and both heads were pushed atomically; #7192's CI is likewise dispatched and pending.

Status, stated plainly: the claim-1 code is fixed and published at f66687ef, CI on the new heads is dispatched and not waited for, and the changes-requested decision stands until re-reviewed — this is not an all-resolved or ready-to-merge claim. The selection-retargeting finding is answered separately in the F2 response and remains owned by its own PRs (#7197/#7239).

Update — 2026-09-09 20:13Z, current-CI evidence addendum (supersedes the "CI dispatched / pending" wording above; all original claims above are unchanged): the failed-jobs-only retries of both runs, on the same two heads with no new commits pushed, have since completed green.

  • fix(desktop): refresh @ search when channel membership changes #7191 (this PR) @ f66687ef989f14bfa026b16d4a35def9beb74b58 — CI run 34382606766, attempt 2 (failed-jobs-only retry), completed, success, 2026-09-09T19:38:30Z. The previously failed jobs now pass: Desktop E2E Integration (1/2), Desktop E2E Integration (2/2), and the Desktop E2E Integration gate; all other applicable jobs are green or path-skipped.
  • fix(desktop): show an agent in @ search after you add it to a channel #7192 (stacked replay) @ e7b84342274c5278a03f4ccb8cfd02de8269729b — CI run 34382607004, attempt 2 (failed-jobs-only retry), completed, success, 2026-09-09T19:38:43Z. The same integration shards and gate pass; its current applicable checks read green.
  • The superseded attempt-1 failures were setup-only — both integration shards failed at the Install Playwright system dependencies step, not in test logic. As of this update, fix(desktop): show an agent in @ search after you add it to a channel #7192's checks listing also still carries cancelled contexts from the earlier cancelled run 34382606208, superseded by the current runs; no claim is made here about fix(desktop): show an agent in @ search after you add it to a channel #7192's raw PR-level rollup field — the linked attempt-2 run results are the evidence.
  • Scope: remote-CI evidence only. No new local tests were run, and the local 6031/6031 desktop-suite proof above stays attributed to its documented prepublication validated tree (byte-identical to the published commit), not re-run at the commit. The changes-requested decision stands pending re-review: this update asserts CI completion only — not approval, re-review, or resolution of any review thread.

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.

2 participants