Repository navigation
fix(mentions): stop per-keystroke member scans in large channels - #1828
Merged
Merged
Conversation
Typing in any composer in a large channel (#general, ~2,500 members) rebuilt every mention candidate on each keystroke, even with the @ chooser closed: useTeamMentions called mentionCandidates unconditionally, and that build was O(members x agents) with per-member find/filter/some scans. - Build team eligibility only for shown team rows of an open chooser. - Cache mentionCandidates per session/channel on its immutable source snapshots, and build it in one keyed pass. - Reuse labeled choices and local keys per cached candidate list. - Match queries through a sorted index of name starts (matchingMentions), so an @ keystroke costs O(log n + matches) instead of ranking every candidate. - Compute composer chip names only when recipients change. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
The draft recreates its recipient array on every edit, team rows and the chooser shared one candidate cache slot, and every empty directory page was a fresh array. Each one rebuilt every label and the name index on an @ keystroke. Name on recipient facts, keep a slot per directory array, share one empty page, and keep rows for only the latest few channels. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
Most directory hits in a large channel are members a profile already names; they change no row, alias, key or label. Pass only the other people, interned by content, so a page of members rebuilds nothing and each settled page or carried merge with the same outsiders reuses one build instead of rebuilding over every member up to three times. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
loganj
marked this pull request as ready for review
October 28, 2026 16:46
Profile names are member input of any length. Each index entry copied the rest of the name at every word start, so a many-word name grew the index with the square of its length. Cut entries to 32 code points and keep at most 16 per name; a name with more is matched directly by ranking. Also fail a test when candidate building reads a session store that its cache does not compare. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
kalvinnchau
approved these changes
Oct 28, 2026
matt2e
added a commit
that referenced
this pull request
Oct 29, 2026
`team-mentions.test.tsx > directory pages rebuild the name index only for people they add` (0bc0752, #1828) failed on every run of this branch since the rebase onto main: `people` was called once, not three times. It assumes every keystroke reads the directory. Its mock returned `hasMore: false`, so on this branch the complete `p` page answers `pe` and `per` through `prefixPage` (95050d1) with no read, and the later `pers` step, which swaps an outsider into the page, would be served from the cache too and never see it. The mock now returns truncated pages, as 95050d1 did for the other tests where a read is the behavior under test, so each keystroke still reads and the test checks what #1828 meant it to. No product change. Verified: team-mentions.test.tsx 13/13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
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
Measurements
Local headless Chromium, dev build, against real #general data. Typing 43 characters into the #general composer:
A small channel takes 22 ms per character, so channel size no longer adds cost. Typing
@loganin #general dropped from 118 ms to 62 ms per character; mention CPU went from 260 ms to 69 ms, most of it the one-time index build on the first@.Details
mentionCandidatescaches its rows per session and channel. The cache key is the immutable snapshots it reads: the channel row, profiles, agent choices, archives, roster and directory page. Each directory array has its own entry, so the chooser and its team rows do not evict each other, and only the latest few channels and directory pages keep rows. The rows rebuild only when one of those changes, not on each keystroke. Labels are cached by the same rows plus the draft's selected recipients, compared by key and name, because the draft recreates that list on every edit. Every empty directory page now shares one array. The build itself is now a single keyed pass. Before, it did afind,filterorsomescan over agents and directory people for each member.matchingMentionskeeps a sorted list of each choice's name starts: the folded name and every later word start, as the matching rules insrc/bundled/mentions/README.md§3 define. A binary search returns only the choices that can match, at a cost of O(log n + matches). Ranking and Space selection then apply the unchanged rules to that subset. A test runs every portable ranking and Space fixture through the index and checks that the results equal the full-list results.directoryChangesleaves them out, and the rest are compared by content, not by array. A page of members rebuilds nothing. Repeated pages and the chooser's merged page lists with the same outsiders reuse one build. A test checks that the rows are equal with and without the people it leaves out.@query that remounts on each keystroke, with a saved team shown, a new recipient array and settled empty directory searches, rebuilds no name starts (the old code rebuilt 600). Pages of members rebuild nothing, and a page with an outsider rebuilds once while it stays the same (the old code rebuilt 600).Not changed:
@query still checks all names once per keystroke to decide whether it is a search or prose.🤖 Generated with Claude Code