Repository navigation
feat(profiles): reserve profile room for an open channel's roster - #2269
Conversation
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 the reservation approach looks right to me: the largest open roster sets the limit, the deferred shrink keeps profiles across a remount, and the scoped naming lookup matches the naming spec. there are four blocking issues inline, mostly where the bigger cache meets the mention demand budget and the live transport, plus three nonblocking nits.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two additional findings on cache headroom and naming-policy ownership.
The profile cache held at most 1,024 profiles. In a channel with more members than that, the mention menu could load only the first 1,024 names, and loading them pushed every other cached name out, so names elsewhere fell back to hex keys. An open channel now reserves room for its whole roster on top of the 1,024-profile base, up to 10,240 profiles in total. Leaving the channel returns the limit to 1,024 and evicts the least recently requested profiles at once. The verified-name cache follows the same limit. The mention menu loads the whole roster up to the reserved limit, and asks again for names the cache evicted after it first requested them. Scope labels now look up only the scope's own keys and their agents' owners instead of passing every cached identity to the naming policy, so labelling a channel no longer costs time in proportion to the whole profile cache. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
…request order Review of the roster reservation found three ways the cache lost names it should keep or kept room it should give back: - A roster count change ran the effect cleanup before the new reservation, so the cache shrank to its base and evicted the roster it had just loaded. A release now shrinks after the current task, so a release and a new reservation in one commit only resize to the new size. - A retained thread tab in the background held its reservation until it closed. A conversation now reserves room only while it is active. - The verified cache shrank its head store, which evicts by arrival order, before it trimmed by request order. A recently requested verified name could lose its head and never be asked for again. It now trims first and treats any head the byte budget drops as evicted. Also: the known-unnamed list shrinks with the cache, the mention menu does not reread known-unnamed members each time it opens, Space completion asks for up to the current limit, and the large cache tests use unsigned profiles so they stay fast under a loaded run. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
… step Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
… cancel stale reads on shrink Review of the roster reservation found these gaps: - The mention menu counted every earlier request in the session against the current roster's allowance. After a first large channel, a second one loaded only part of its roster. Only this roster's own earlier requests count now. - Verified-name syncs shared the 32 artifact sync slots, at 128 people each, so verified names stopped at about 4,096 people and a large roster could refuse artifact views. They now have their own pool of 80, enough for the 10,240-profile limit. - A shrink left the verified syncs it emptied open until the next ensure. It now stops them at once. - A background read still in flight when a channel released its room landed after the shrink as the newest entries and could push out names the next view had just loaded. A shrink now cancels those reads and counts them with evictions, so the mention menu asks again within the new limit. Foreground reads and reconnect refreshes keep going. - The channel reserves in a layout effect, so the mention menu reads the grown limit in the same commit the roster arrives. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
A 9,000-member channel showed 16 frames over 50 ms while the mention menu loaded its roster. The profile listeners were not the cost: the open menu re-ranked every name on each 500-profile page, about 60 ms per publish. - A background profile read of many pages now publishes once, when its pages settle. The session accepts each page before the read returns it, so the directory recognises a page by its pending batch. A foreground read that joins the batch still publishes at once. An accepted page counts as loaded before its publish, so asking again reads nothing. - The channel avatar preview reads at most the first 1,024 members. With room reserved for the roster, it had read all 9,000 profiles just to show three avatars. - A new Browser measurement opens the mention menu in a 9,000-member channel, checks each member is read once, and records frame gaps for the load and for leaving the channel. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
…t the cap - The folded publish now applies only to events that answer the directory's own background read: the session passes the read's signal to accept. Live traffic publishes at once, so a profile update (an agent owner revocation) no longer waits behind a read that has not finished. - A roster loads into the room its channel reserved (limit minus the 1,024 base), in the mention menu and in Space completion. A roster past the 10,240 cap no longer evicts the viewer, visible authors and DM names. With no reservation a view may still use the base. - scopeKeys in the naming policy owns the scope rule (a selection plus its agents' owners). The name directory looks up those rows with it instead of keeping its own copy, and the policy contract says a scope's input holds only those rows. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Logan Johnson <[email protected]>
2544926 to
a1f623a
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 all of my earlier comments are fixed in a1f623a, and I checked each fix against the code. the 9,000-member measurement covers the frame-cost question. the one long frame left comes from mention ranking being linear in the roster, which the description calls out, and I think it is fine to follow up separately. lgtm
* commit '05fba2455': feat(agents): show each session's system prompt and dividers in activity (#1855) feat(bestie): hourly check-ins from your Bestie (#2037) Authorize local apps to use Buzz MCP tools (#2211) feat(bestie): choose the private channels Bestie can read (#2036) Polish message actions, spacing, and membership notices (#2258) test(unread): prevent startup catch-up in receipt reserve journey (#2274) test(search): wait for rejected join clearance to settle (#2278) test(reactions): provide stable emoji media for icon rendering (#2279) feat(profiles): reserve profile room for an open channel's roster (#2269) Signed-off-by: Daniel Rouleau <[email protected]> Co-authored-by: Codex <[email protected]> # Conflicts: # src/bundled/codex/runtime.ts
…ection * origin/main: (39 commits) Authorize local apps to use Buzz MCP tools (block#2211) feat(bestie): choose the private channels Bestie can read (block#2036) Polish message actions, spacing, and membership notices (block#2258) test(unread): prevent startup catch-up in receipt reserve journey (block#2274) test(search): wait for rejected join clearance to settle (block#2278) test(reactions): provide stable emoji media for icon rendering (block#2279) feat(profiles): reserve profile room for an open channel's roster (block#2269) feat(host): harden byte lifecycle and add protected image upload (block#2199) Updated onboarding (block#2183) perf(relay): read each signed roster once in large channels (block#2244) AIDA-2346: Keep spoilers hidden in reminder previews (block#2261) fix: AIDA-2371 Keep sidebar selection pill aligned during resize (block#2182) fix(relay): skip background rereads of profiles a read found absent (block#2147) fix: prevent startup logo selection (block#1823) fix(agents): send sign-in evidence with agent Git credentials (block#2240) perf(mentions): stop scanning every member on each mention keystroke (block#2246) fix(attachments): send image dimensions so previews don't shift (block#2262) feat(shortcuts): add Slack-style hotkeys (block#2170) perf(plugins): share host request clients across calls (block#2243) fix(identity): keep federated sessions and agent grants in one keychain item (block#2225) ... # Conflicts: # src/features/messages/ThreadPanel.tsx
…ock#2269) 🤖 ## Summary - In channels with more than 1,024 members, the app could not keep every member's name in memory. The profile cache holds at most 1,024 profiles. So the mention menu loaded only the first 1,024 members. Loading them also pushed every other cached name out, and names elsewhere in the app changed to a short hex key. - Now an open channel reserves room for its whole member list on top of the 1,024-profile base, up to 10,240 profiles in total. When you leave the channel, the limit goes back to 1,024 and the app removes the profiles nobody has asked for recently. The room is temporary: the cache stays at 1,024 when no large channel is open. - The mention menu now loads the whole member list into the reserved room. It asks again for names the cache removed after it first asked for them. It does not ask again each time it opens for members already known to have no name. - Labeling a channel's members now looks up only that channel's own people (and the owners of its agents). Before, it passed every cached profile to the naming rules. So labeling no longer gets slower as the profile cache grows. ## Details - `profile-directory.ts`: `reserve(members)` returns a release function. The limit is `min(10_240, 1_024 + largest reservation)`. A grow takes effect at once. A release shrinks after the current task (a `setTimeout`), so a release followed by a new reservation in the same React commit (a remount or a member count change) only resizes to the new size and keeps the loaded profiles. A shrink also trims the known-unnamed set and publishes once. - `budget.ts`: the byte budget is now the entry limit × 2 KiB (2 MiB at the base, 20 MiB at the cap), so a larger limit is not cut short by the old fixed 2 MiB budget. - `verified-profile-directory.ts` follows the same limit. On a shrink it trims by request order first, then resizes its signed-head store. Any head that the byte budget removes is treated as evicted, so a later request fetches it again instead of keeping a name that can never be checked. - `Conversation.tsx` reserves only while the conversation is active. A retained background tab gives its room back. - `MentionCompletion.tsx` skips known-unnamed members unless the user clicks Retry, and starts over when the cache reports evictions. Space completion asks for up to the current limit, not a fixed 1,024. - `identity-names/directory.ts` builds a scope's labels from the scope's own keys and their agents' owners. - `docs/channels.md` and `src/bundled/mentions/README.md` describe the new limit and the mention menu rules. - Tests use a new `unsignedProfiles()` fixture for cache-size tests, so the large-cache tests stay fast. The verified-cache shrink test still uses real signed events. ## Review fixes - The mention menu's allowance counts only this roster's earlier requests, so a second large channel loads its whole roster. A roster loads into the room its channel reserved (limit minus the 1,024 base), so a roster past the 10,240 cap keeps other names cached. Space completion uses the same room. - Verified-name syncs have their own pool of 80 (enough for 10,240 people at 128 each), apart from the 32 artifact syncs. A shrink stops the syncs it emptied at once. - A shrink cancels background profile reads still in flight. `evictions()` counts them, so the mention menu asks again. - `Conversation.tsx` reserves in a layout effect, before a child's effect reads the limit. - A background profile read of many pages publishes once, when its pages settle. The session passes the read's signal to `accept`, so only that read's own pages wait; live traffic and a foreground read that joins publish at once. An accepted page counts as loaded before its publish. - The channel avatar preview reads at most the first 1,024 members. With the reserved room it had read a whole 9,000-member roster to show three avatars. - `scopeKeys` in the naming policy owns the scope rule; the name directory uses it. ## Measurement New Browser measurement `profile-roster-scale.spec.mjs`: a 9,000-member channel, open the mention menu until every member is read, then leave the channel. It checks each member is read once. | Chromium | frames > 50 ms | longest frame | time past 60 Hz | |---|---|---|---| | main (loads 1,024) | 1 | 60 ms | 126 ms | | before the fold | 16 | 117–133 ms | ~970 ms | | this PR | 1–2 | 133–150 ms | 168–201 ms | WebKit: main 3 frames over 50 ms (longest 110 ms); this PR 2 (longest 131 ms). Leaving the channel: no frame over 50 ms in Chromium, one 51 ms frame in WebKit. The profile listeners were not the cost (label refold is ~6 ms per publish at 9,000). The open mention menu re-ranked every name on each 500-profile page. The one remaining long frame is a single re-rank of 9,000 names. ## Not in this PR - Mention ranking is still linear in the roster, so one publish at 9,000 members costs one ~130 ms frame while the menu is open. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <[email protected]> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
…2395) ## What users see Someone mentions you in a channel you haven't opened this session, and their profile hasn't loaded yet. You leave any channel and open the **Inbox**. The row shows a shortened key (`npub…srl`) and a letter avatar instead of their name. It stays that way while the Inbox is open, and nothing asks again. Their name appears only after you open their channel. This is a regression from #2269 ("reserve profile room for an open channel's roster", by Logan Johnson, `loganj`). A bisect over 248 commits, with no skips, found its merge commit `fe6eaa222` as the first bad one. Its parent `589c2a489` names the author right away. The channel being left doesn't need to be large: in the repro, #Alpha has one member. https://github.com/user-attachments/assets/e1e24cfb-14c1-4a4c-ba65-993443e28b5c Left: the branch base 8668dbd. Right: this branch at b3d1f09. Both sides click Inbox on the same frame. On the left, the Inbox's read for Priya is cancelled 6 ms after it's sent and the row stays `npub…srl`. On the right, the read returns 200 in 16 ms and the row shows "Priya Shah" from the first Inbox frame.  ## Cause 1. An open channel reserves profile-cache room for its roster (`Conversation.tsx`, a layout effect). Leaving releases that reservation, and the release schedules the shrink one task later (`shrinking ??= setTimeout(resize)`). 2. In the same commit, the Inbox's mount effect asks for the names it shows, as a background read. 3. When `resize()` runs, it treats the shrink (1025 → 1024 in the repro) as a reason to cancel **every** pending background read that isn't a reconnect refresh. That includes the read the Inbox just started. 4. The cancel is silent: `ensure()` resolves normally, and only `evictions()` moves. The mention menu is the only consumer of `evictions()`. The Inbox's effect has no dependency that changes, so it never asks again. The cancel in #2269 was right for the departing view's reads, for example the mention menu's 500-author roster pages, which would otherwise land late and push the next view's names out of the 1,024-entry cache. It was wrong for reads the **next** view started while the shrink was waiting. ## The fix The fix is in one production file, `src/features/relay/profile-directory.ts` (+28/−8): - **Stamp at creation.** A counter goes up each time a reservation is released. A read records that count when it is created, and the stamp never changes (`readonly releases`). - **One predicate.** `shrinkCancels(batch)` is true for a background read that isn't a reconnect refresh and was created before the latest release. `resize()` uses it to choose what to cancel. `ensure()` uses it to decide whether a request may join a pending read. - **No joining a read that's about to be cancelled.** While a shrink is waiting, a request doesn't join a pending read that the shrink would cancel. That name gets its own read, and the shrink still cancels the rest of the departing page. Joins on foreground and refresh reads, and any join while no shrink is waiting, work as before. - A remount or a roster-count change is unaffected. `reserve()` clears the waiting shrink before the children's effects ask for names. - The `evictions()` doc comment, the `resize()` comment and one line of `docs/channels.md` now describe the narrower cancel. `docs/buzz-help.md` is unchanged, because this restores the earlier behaviour. **Why the stamp isn't updated on a join.** The first commit (8429c27) updated a pending read's stamp whenever a later request joined it. Review showed that this kept a whole departing roster page alive for the sake of one shared author. The reader runs one background job at a time, so those pages also landed before the Inbox's own read. In a 9,000-member scenario, 3 joined pages loaded 1,500 roster profiles into the 1,024-entry cache. That pushed out 5 names the Inbox had already shown, and those rows went back to `npub…`, which is the case #2269 was written to prevent. With the creation-time stamp and the join skip, the same scenario cancels 19 pages and keeps all the Inbox's names and all 200 earlier names, with the same 20 reads. **Rejected alternatives** - **Make the Inbox re-read when `evictions()` moves.** That fixes one page, and every switch from a channel to the Inbox would send the read, cancel it and send it again. - **Bump `generation()` when a shrink cancels reads.** That changes what `generation()` means: it marks a full cache purge, which other code keys on. - **Drop the cancel, or skip it for small shrinks.** Either one brings back the eviction case #2269 fixed. **Accepted costs.** These were noted in review and don't block: - A release that leaves a larger reservation open can split a join into a second, redundant read until the next task. - If the next view asks again for every author on a departing page, that page isn't cancelled. It loads only names the next view asked for, so the cost is one redundant read and no names are pushed out. ## Surfaces covered The directory change covers every page that replaces a channel and asks for names in its mount effect without watching `evictions()`: - the Inbox (tested and recorded); - Drafts, opened straight from a channel (`DraftsView.tsx`); - Reminders (`RemindersPage.tsx`); - Workflows owner names (`WorkflowLanding.tsx`); - Agents → Watching (`watching/Tab.tsx`); - the Inbox hover preview in open PR #2347, which copies the Inbox's effect. These were found by reading the code. Only the Inbox has a browser case or a recording. The others use the same directory rule, which the unit test covers. **Out of scope.** Views that stay mounted when you leave a channel are still exposed if their read happens to be in flight at that moment: - sidebar DM labels (`useChannelLabels.ts`); - Inbox names for edited-message authors (`unread.ts`); - channel previews loaded in the background (`store.ts`), which are re-read when the channel opens. The window is one relay round trip at the moment you leave. None of these was reproduced, so a follow-up is warranted only if one is. ## Tests ### Unit test (Vitest), `src/features/relay/profile-details.test.ts` The new test, "keeps background reads asked for after a release when the shrink runs", sits next to #2269's cancellation test, which is unchanged. - **Setup:** reserve room. Start two departing background reads: one for a `stale` author, and one page holding `joined` and `other`. Release. Before the timer fires, the next view asks for `fresh` and `joined`. - **Assertions:** - how each read is split: `[stale]`, `[joined, other]`, `[fresh, joined]`; - `evictions()` moves by exactly 2, because both departing reads are cancelled; - every read is answered with all four profiles, and only `fresh` and `joined` end up in the cache. - **Mutants:** in the round 2 review, four mutants were run against this test at b01fe78, and each failed: 1. the first commit's stamp update on join: evictions +1, and `other` is named; 2. no join skip: `joined` stays unnamed; 3. a shrink that never cancels: evictions +0, and `stale` and `other` are named; 4. main's behaviour: `fresh` and `joined` stay unnamed, with evictions +3. Mutants 1 and 2 issued 3 reads either way, so b3d1f09 added the read-split assertion. With the join skip removed, the third read holds only `fresh`, and the test now fails on the split too. ### Browser case, `tests/browser/inbox.spec.mjs` One case was added, "Inbox names after leaving a channel › names an author seen only in the Inbox", and none were removed. - **The flow:** open on #Alpha, click Inbox, and expect the row for an author seen only in #Launch to contain "Priya Shah". Also expect exactly one kind-0 query naming her. - **Why it needs a browser:** the bug depends on ordering across two surfaces: the channel's layout-effect release, then the Inbox's mount-effect read, then the deferred shrink. The unit test proves the directory rule, not that the real Inbox's read starts after the release and before the shrink. - **Fail, then pass:** it fails without the fix in both Chromium and WebKit, where the row shows `npub…`. It passed 5/5 per engine at 8429c27 and passed again in both engines after the join change. Production code hasn't changed since then; b3d1f09 changes only the unit test and a comment. - **Why CI missed this:** no journey checked the name of an author seen only in the Inbox. ### Other checks during development These were not re-run for this push: - `profile-details.test.ts`: 25/25 pass. - The 16 unit test files that import the profile directory: 630/630 pass. - `profile-roster-scale.spec.mjs`, #2269's 9,000-member check: passes in both measurement projects. - `typecheck`, `lint` and `git diff --check`: clean. - Pre-commit hooks: passed on each commit. Pre-push hooks were skipped for this push (`--no-verify`). ## Review rounds - **Round 1 (8429c27):** one blocker. Updating the stamp on a join kept departing roster pages alive (the 9,000-member scenario above). Fixed in b01fe78, along with the optional items: a clearer field comment, a `stale` assertion that can actually fail (the test now answers every read with all profiles), and a minimal `docs/channels.md` diff. - **Round 2 (b01fe78):** the blocker is fixed, with no new defects, and the change was judged at the 9/10 bar. Its optional items, asserting how each read splits and a comment wording, are in b3d1f09. ## Status This is a draft until a human tries it in the app (AGENTS.md "Before opening a PR", step 3). To test: open the app on a channel, then click **Inbox**. An author who mentioned you in a channel you haven't opened this session should show their name and avatar right away, without opening their channel. Deferred: - the human test above; - hosted CI on this head; - a WebKit recording (the browser case covers WebKit); - recordings of the other covered pages. Logan Johnson (`loganj`): this narrows #2269's cancel-on-shrink and changes the `evictions()` contract wording. Your review is welcome. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
## Problem Each Cmd+K keystroke ranks every cached profile against the typed text. `personMatch` calls `matchPerson` for each of a person's shown and hidden names. `matchPerson` folds the whole name on every call: NFKD, two regex replaces and a lowercase per code point, plus array spreads. It also folds the query again for every name. #2322 added a verified full name and profile names to each person. #2269 raised the profile cache ceiling from 1,024 to 10,240. Together they made this per-keystroke pass several times larger, and nearly all of it re-folds names that did not change since the previous keystroke. ## Change - `matchTier` returns the tier `matchPerson` would give. It reads folded names from a module-level cache that holds at most 65,536 names and is cleared when full. Names that fold to more than 128 code points are not cached. - `matchPerson` and `matchTier` share the matching body (`search`), so tier rules live in one place. `matchPerson` still builds its own fold with origins for underlining. - `rankPeople` folds the query once per ranking and passes it to `personMatch`, which uses `matchTier`. Before, `personMatch` refolded the same query for every person: 10,240 folds per keystroke with a full profile cache, about 2.4–4.4 ms for a 2–4 character query in a Node micro-benchmark. This last step is not in the table below. - `nameStarts` and the cache share one fold helper. ## No behavior changes `matchTier` runs the same search over the same folded characters as `matchPerson`, so people rank exactly as before. A new test checks that `matchTier` gives `matchPerson`'s tier for repeated names and changing queries, including accented, compatibility, final-sigma and padded names. Underlined positions still come from `matchPerson`. ## Evidence Two benchmarks compare main and this PR: - **Browser.** A production build runs in Chromium at 4× CPU throttling. The rows are keydown → next-paint medians (Event Timing) for typing in Cmd+K, with names that miss and names that match people, and for typing in the composer. Main `8c0cb0c1e` and this PR were run as six serial, interleaved pairs on one Linux and one macOS workstation. All keys matched in every run. - **Render.** `SearchResults` renders in jsdom with 1,700, 5,000 or 10,240 cached profiles (accented and plain names, half with verified full names). It types `a`, `al`, `ale`, `alex` (cold), then repeats the same keystrokes (warm). Values are the median time to render one keystroke. Main and this PR were interleaved on 2 Linux and 2 macOS workstations, with 16 samples per cell. | metric | Linux: main | Linux: this PR | Linux Δ % | macOS: main | macOS: this PR | macOS Δ % | |---|---|---|---|---|---|---| | browser: Cmd+K, names that miss | 128 ms | 104 ms | −18.8% | 88 ms | 72 ms | −18.2% | | browser: Cmd+K, people | 144 ms | 112 ms | −22.2% | 96 ms | 80 ms | −16.7% | | browser: composer, `@` mention | 448 ms | 440 ms | −1.8% (noise) | 272 ms | 272 ms | 0% | | browser: composer, plain prose | 24 ms | 24 ms | 0% | 16 ms | 16 ms | 0% | | render: 1,700 profiles, cold | 23.0 ms | 16.0 ms | −30.4% | 13.8 ms | 10.0 ms | −27.8% | | render: 1,700 profiles, warm | 16.3 ms | 9.5 ms | −41.8% | 9.7 ms | 6.0 ms | −38.3% | | render: 5,000 profiles, cold | 39.3 ms | 22.5 ms | −42.6% | 22.2 ms | 13.0 ms | −41.5% | | render: 5,000 profiles, warm | 34.7 ms | 15.5 ms | −55.4% | 19.9 ms | 9.3 ms | −53.1% | | render: 10,240 profiles, cold | 69.1 ms | 32.7 ms | −52.6% | 39.5 ms | 19.8 ms | −49.8% | | render: 10,240 profiles, warm | 65.8 ms | 25.7 ms | −61.0% | 37.5 ms | 15.5 ms | −58.6% | Lower is better. - Event Timing rounds durations to 8 ms, so the Linux mention change is one rounding step and counts as noise. - The render benchmark ran before two changes: the bypass for names over 128 code points and the shared fold helper. Neither affects names as short as these. Checks: the search, mentions, shell and channel-members unit suites pass, along with `tsc` and `biome`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Kalvin C <[email protected]> Signed-off-by: Kalvin Chau <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖
Summary
Details
profile-directory.ts:reserve(members)returns a release function. The limit ismin(10_240, 1_024 + largest reservation). A grow takes effect at once. A release shrinks after the current task (asetTimeout), so a release followed by a new reservation in the same React commit (a remount or a member count change) only resizes to the new size and keeps the loaded profiles. A shrink also trims the known-unnamed set and publishes once.budget.ts: the byte budget is now the entry limit × 2 KiB (2 MiB at the base, 20 MiB at the cap), so a larger limit is not cut short by the old fixed 2 MiB budget.verified-profile-directory.tsfollows the same limit. On a shrink it trims by request order first, then resizes its signed-head store. Any head that the byte budget removes is treated as evicted, so a later request fetches it again instead of keeping a name that can never be checked.Conversation.tsxreserves only while the conversation is active. A retained background tab gives its room back.MentionCompletion.tsxskips known-unnamed members unless the user clicks Retry, and starts over when the cache reports evictions. Space completion asks for up to the current limit, not a fixed 1,024.identity-names/directory.tsbuilds a scope's labels from the scope's own keys and their agents' owners.docs/channels.mdandsrc/bundled/mentions/README.mddescribe the new limit and the mention menu rules.unsignedProfiles()fixture for cache-size tests, so the large-cache tests stay fast. The verified-cache shrink test still uses real signed events.Review fixes
evictions()counts them, so the mention menu asks again.Conversation.tsxreserves in a layout effect, before a child's effect reads the limit.accept, so only that read's own pages wait; live traffic and a foreground read that joins publish at once. An accepted page counts as loaded before its publish.scopeKeysin the naming policy owns the scope rule; the name directory uses it.Measurement
New Browser measurement
profile-roster-scale.spec.mjs: a 9,000-member channel, open the mention menu until every member is read, then leave the channel. It checks each member is read once.WebKit: main 3 frames over 50 ms (longest 110 ms); this PR 2 (longest 131 ms). Leaving the channel: no frame over 50 ms in Chromium, one 51 ms frame in WebKit.
The profile listeners were not the cost (label refold is ~6 ms per publish at 9,000). The open mention menu re-ranked every name on each 500-profile page. The one remaining long frame is a single re-rank of 9,000 names.
Not in this PR
🤖 Generated with Claude Code