Repository navigation
fix(channels): bound large member list rendering and profile reads - #2028
Conversation
Page current-member groups and keep large rosters usable while optional names load in bounded background batches. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Found a profile-retention regression in large member lists.
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Reviewed 947428d.
The earlier shared-profile eviction finding is repaired in the exercised opening/paging flows with 1,100 named members. All 85 dialog tests pass locally; the capacity regression now completes in 1.33s. Chromium and WebKit checks also pass for paging, retained conversation names, search, focus, reload, profile-read recovery and reconnect.
One remaining uncertainty is classification/name-search coverage when roster prewarming fails. ChannelMembersDialog.tsx:793,830,873,966 uses displayed-page profiles and frozen grouping, which can leave component-only counts incomplete. However, the normal 126-member app path preloads profiles through ChannelMemberAvatars.tsx:38–43 and produces correct People/Agents counts in both engines, including after releasing delayed profiles and refreshing. This is an unproven concern, not an established defect; failed-prewarm and alternate-entry variants were not validated.
Hosted CI was still pending in the completed review evidence. Real relay/native behavior and offline-only persistence were not exercised. No new confirmed defect beyond the previously reported and repaired eviction issue.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord automated source review via Wes’s account.
[P3] Remove the internal tracker URL from the public PR description. The Why section links to an issue in the company’s private tracker, exposing an internal workspace/task reference on this public repository. Remove the internal URL and keep the self-contained problem statement (or link a public issue). This finding concerns the published description, not contributor attribution.
No additional actionable code defect found in the six-file patch and traced paging/profile-directory, administration, focus and retry paths.
Reviewed head 947428dd779ac77ab7b01d0d3c90022b87615aff; target base 4e122dde85de331f7a1a59e06b5f9db42e28f8e1; merge-base 6ee523c831140a5a79e2e778af54c2672bee64b7. Source-only: no tests, builds or app execution performed. Both attached screenshots and commit messages were inspected. The linked issue could not be read (403). One hosted CI snapshot showed successful checks with Windows native validation skipped; this does not verify the reported native hang, live performance, screen-reader behavior or human acceptance. Added test runtime cost was not measured. This is a non-blocking COMMENT review, not approval.
Use relay name-search results when filtering existing members and allow large non-DM roster searches without add-member permission. Reuse paging and error controls without changing DM behavior. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Preserve current member-dialog controls and busy states alongside bounded member pages and uncached name search. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Observe member-list busy state and reopen to rebuild groups after main removed the always-visible refresh button. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Bring in the attention test corrections for fresh CI. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Include current message-link labels and relay thread recovery. Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One non-blocking loading-state issue found.
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Reviewed through c746b4f.
Earlier head (535edc8): any change to the roster reset paging to page 1, because memberPages reset whenever memberKey changed. The roster updates live from the relay while the dialog is open, so an admin who removed someone on page 8 landed on page 1 and lost their focus. In a large channel, anyone browsing got sent back to page 1 when someone else joined or left.
c746b4f fixes this the way I'd have suggested. Paging now resets only when the search text or role filter changes. The existing clamp handles a shorter roster, and Previous/Next work from the clamped page. The new test covers both a removal on page 2 and a roster that shrinks below the current page. I have no remaining concerns.
Non-blocking, no change requested: on a first open with an empty cache, a large roster is sorted by public key, and that order holds while names load, so page 1 won't look alphabetical. Sorting by name would mean loading every profile, which is what this PR avoids. The docs already describe this.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One page-retention regression remains.
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.
No new actionable code defects found in the added member-name search, loading feedback and page-retention changes. The persisted page clamp addresses the previously reported shrink→growth regression; the added source tests cover subsequent growth and crossing below one page.
The previous P3 public-description finding remains unresolved: the Why section still contains the internal tracker link. Please remove that link while retaining the self-contained problem statement. No duplicate inline finding is added.
Reviewed head ec69c6ea1e2296aab0c4c84990e65f82c51dc2bb; target base ffa65d87bb13c85284b3d0fbd6c96a8fc2008ea8. Source-only: no tests, builds or app execution, CI assessment, measured performance or runtime accessibility validation. Both attached screenshots and all 21 feature/merge commit messages were inspected. The linked issue could not be read (403). The reported native hang, whole-app profile traffic, live focus/assistive-technology behavior and human acceptance remain unverified. Non-blocking COMMENT review; no approval.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Carl, an automated reviewer, commenting via Wes's GitHub account. Royal Court review: UX/a11y (Princess Donut), data contract (Mongo), challenge pass (Mordecai), integrated by Carl.
Verdict: one P2 and two P3s, all in the inline threads. Request size, relay search merging, abort and stale-reply guards, page clamping and focus return all hold. We found no new data-contract or state-loop defects.
Merge criteria: keep a live roster change from reshuffling the page being browsed, or have the author and product owner explicitly accept that tradeoff. The P3s are optional.
Validation: the P2 was reproduced at this head with a mounted Vitest case (1,100 named members, browse page 2, wait for names, remove one off-page unnamed member): 26 of 50 rows changed. Everything else is source review. We didn't rerun the suites, since CI is green. Native hang, live assistive-technology behavior and human acceptance remain unverified. The earlier P3 about the internal tracker link in the description is still open; it is not repeated here.
Reviewed head ec69c6ea1e2296aab0c4c84990e65f82c51dc2bb.
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
Co-authored-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz> Signed-off-by: John Tennant <[email protected]>
#2028 (merged) bounded large member-list rendering. It already removed the main cost this PR originally targeted. Its sort computes each label once before comparing, rather than calling the label inside the comparator, and the member filter now returns early when the query is empty. After rebasing onto #2028, this PR still covers the per-render repeats that #2028 left in place. The dialog still derived each members Signed-off-by: Tom Brow <[email protected]> Co-authored-by: Tom Brow <[email protected]> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
…2216) ## Problem Opening the members list of a ~1,175-member channel froze the desktop app for minutes. Client metrics from one click recorded main-thread long tasks of 65.5 s, 34.2 s, 15.8 s and 13.6 s in a row: 206 s of long tasks in a 6.5 min session, with 605,689 `fold` calls. The socket starved in the meantime. A foreground read timed out after 23.9 s, the live connection was declared unresponsive and reconnected, and the dialog showed "Relay read timed out" / "The member list could not load". The trace shows 25 full 128-subject kind 30382 batches (1,290 events). Every verified-profile change called `publish()` synchronously inside the socket callback. Each `publish()` refolds every profile, runs the namesake scan and notifies every listener (views then re-render), and nothing yields between frames. #2209 cut replay notifications to one per batch EOSE. Those were still synchronous, as were every live change, disconnect, reschedule and failure. ## Change `profile-directory.ts`: the verified directory's change callback now schedules one `publish()` per macrotask (`setTimeout`) instead of calling it inline. Changes within one task (a disconnect fan-out, a reschedule, one relay frame), plus frames already queued before the timer fires, coalesce into one publish. This does not strictly cap publishes across separate frames: `live.ts` delivers each WS frame as its own task, so frames arriving after the timer fires get their own publish. `dispose()` cancels a pending publish. Not changed: kind 0 `accept()` and `clear()` still publish synchronously. `session.ts` and `store.ts` treat a profile notification as a synchronous boundary (subscribers can revoke or clear the session inside it), and those paths are already one publish per read or batch. Behavior change: verified overlays reach `snapshot()` one macrotask after the relay frame instead of inside it. Hidden or occluded webviews throttle `setTimeout`, so while the app is backgrounded, verified changes (retractions, the switch to "cached" on disconnect) reach `snapshot()` later. Only readers outside the rendered UI, such as notifications, would see that delay. ## Related - #2159 makes each publish's namesake scan linear. This PR limits how often publish runs; #2159 limits what each run costs. - #2028 limits the members dialog's name reads to the visible page. ## Validation - New test `coalesces a burst of verified changes into one directory notification`: fails on main, passes here. - Verified-profile and ProfilePanel tests now await the macrotask before reading verified state. - Full Vitest run locally: every file that failed under machine load (load average ~30) passed on an isolated rerun. The pre-push hook timed out on two unrelated UI tests (`App.profile`, `ProfileSettings`, 5 s timeouts); both files pass when run alone (49/49). - Not yet run in the desktop app. ## Open before merge - The build that produced the trace above is not recorded, and #2159 and #2209 are already in base, so this PR has not yet been shown to fix the freeze. Needed: open the ~1,175-member members dialog with client metrics on base and on head, and record total and max long-task time, `fold` and publish counts, and whether "Relay read timed out" recurs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
…sters `profiles.ensure` threw "Profile request exceeds the directory budget" for more than 1,024 ids before reading anything. Community admin raised an unhandled rejection and its name search found nobody, and `from:name` search in a channel failed with the same message. (The Members dialog already pages its name reads, #2028.) The directory now names the first 1,024 ids of a larger request (its cache's entry capacity) and leaves the rest unrequested. Fetching more would only evict names the same request had just loaded. Members beyond the cap keep their public-key label, and no notice is shown, because Try again cannot name them. The over-budget request still reaches verified-profile lookup only for the ids that fit (#2220). Two related partial-failure fixes in the same path: - A batch whose response exceeds the reader's 8 MiB budget retries from the same author in halves, like AgentBrowse's catalog pages, instead of loading zero names. A single author still over the budget fails after one read per halving (at most 10 reads for 500 authors). A foreground promotion rejoins whichever part is in flight. - `ensure` reports a failed batch only after its other batches settle, so a caller that tolerates the failure reads every name that loaded. Callers: Community admin catches the optional name read. Message search `from:` proceeds on a partial directory, as the author picker in SearchResults already does. An unnamed member falls through to the relay name search. Not changed: touch-on-read (`get()` for `event()`/`owner()`). Displayed names come from the folded snapshot, not the LRU, so promoting there would not make eviction follow what is on screen. Both functions are also `useSyncExternalStore` snapshot getters (useAgentOwnerEvidence, useProfileOwner) and run inside projection folds, so `get()` would make pure reads reorder the cache. A use-based policy belongs in ensure() with its own eviction tests. Performance: unchanged for requests of up to 1,024 ids whose batches fit the read budget. Request count, sizes and success latency are the same, plus one array slice. A request above 1,024 ids now costs at most three reads (500/500/24) instead of failing, and asking again with those names cached reads nothing. When a batch fails, ensure now rejects once its slowest batch settles rather than at the first failure. The large-roster `from:` test takes about 1 s locally, which this scale contract needs. Docs: docs/channels.md now says a request above the 1,024-entry budget names those that fit instead of failing. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Follow-up to the capacity-cliff review of 2026-10-29 12:02 UTC. The discovery eviction and Members dialog parts of that review were dropped in the rebase onto main: #1574 replaced discovery's 1,024-entry cap with a 256 MiB byte budget and pins the shortened-id fallback, and #2028 pages the dialog's name reads. Review: threads.ts `reactionsCapped` - Dropped it. A full 500-reaction page forced `limited: true`, which ThreadPanel and MediaReviewViewer show as "Thread history limit reached." even while older replies still load. The target still opens, its 500 reactions are a lower bound, and live reactions keep arriving. relay-queries.md no longer says the view is marked `limited`. Review: halving gaps (a) and (b) - Each part is accepted as it lands. A transient failure on part k retries from part k at the halved size instead of from author 0. Kept because the code is no larger than before (the result array and `.then` go away) and is safe: - retryRead runs attempts one at a time with the caller's signal, so two attempts never share the hoisted cursor. - A part that lands after an abort is dropped, as before. - A promotion still rejoins `batch.reading`. Deliberately not changed: - Warm-cache eviction. ByteLru evicts in insertion order and nothing touches an entry on use. A request that loads enough new names therefore evicts the viewer's own profile and on-screen names, and so does a long session once it has seen 1,024 profiles. This predates the branch at every request size. The fix is a use-based policy in ensure(), or reading only the rows that render, as a follow-up. - Halving is still unmeasured against a real 8 MiB response. Performance: - Profile batches that fit the read budget are unchanged: one accept per batch. A halved batch publishes once per part rather than once per batch, so earlier parts' names show one read sooner. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Follow-up covering the review of 2026-10-29 12:53 UTC. The discovery eviction, channel-link pin and Members dialog parts were dropped in the rebase onto main (#1574 replaced the 1,024-entry discovery cap with a 256 MiB byte budget; #2028 pages the dialog's name reads). Review: profile halving - Dropped. The 2 MiB profile cache keeps only about a quarter of an 8 MiB response, so the parts that landed evicted each other, and the case was never measured. Removed the hoisted cursor, the `ReadError` match and their two tests. The batch read is main's again. - Kept: the 1,024-id cap, the Community admin and `from:` search catches, and settling every batch before a failure is reported. - Docs: channels.md says a batch whose response exceeds the 8 MiB read budget (for example heavy legacy inline avatars) still loads no names, and that reading only rendered rows is a follow-up. Review: threads.ts loadMore guard - Reverted to `!snapshot.canLoadMore || snapshot.limited`. With `reactionsCapped` gone it is equivalent, and the thread diff is now only the separate reaction filter. Tombstone read over the request budget: fixed in code - The target's tombstone read sent every overlay id in one `#e` list. The reader rejects filters over 64 KiB (about 975 ids) before sending, so a target with more overlay ids failed to open. One target read can return 499 safety overlays plus 500 reactions, and `remote` can hold more. Carrying only this response's ids would still exceed the budget. It would also stop re-reading tombstones for overlays already in `remote`. So the read now goes in parts of 900 ids: a `for` loop and a slice in place of the `if`, with no new state. Each part keeps the 500-event fail-closed check. Up to 900 ids it is one request, as before. - Test: the full-reaction-page target test now also carries 480 edits (980 overlay ids). It asserts two tombstone requests of 900 and 80 ids covering every overlay, and the newest edit as the content. Before, the reader rejected the request locally and no tombstone query was sent. Performance: - No read is added anywhere that worked before. Tombstone parts only apply above 900 overlay ids, which used to fail. - Profile batches are back to one accept per batch. - thread-target.test.ts signs its 500 reactions and 480 edits once in beforeAll (about 1.45 s); before, 500 were signed inside the test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Fixes the browser-journey CI failure on PR #2114 (run 41377657528). 2d4007a split the exact-target overlay read into a safety-kind filter and a separate `[7]` reaction filter. The policy relay stub answers only the multi-filter batches it recognises, so the new batch fell through to the sidebar-preferences branch and failed `toHaveLength(5)` in every journey that opens a thread (118 tests). The stub now recognises the batch: two filters on the same single `#e` target, each with `limit: 500`, the first carrying exactly kinds 5, 9005, 39005 and 40003 and the second only kind 7. The fixture's `answer()` serves each filter as before. The 1,100-member Members dialog test this commit also timed was dropped in the rebase onto main (#2028 pages the dialog's name reads). Tests (local): - Chromium and WebKit: right-pane-dismissal, message-navigation, inbox, navigation-thread-history, nested-replies, buzz-links, typing, message-actions, message-management, inbox-tabs, inbox-edit and presence specs, 207 of 208 passed in 8.4 min. The one failure is WebKit message-management.spec.mjs:361 (Delete dialog Close not focused), which also fails locally on main. - channel-opening.spec.mjs on chromium-measurements and webkit-measurements: passed. Not run: the full browser suite. Hosted CI will cover it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…sters `profiles.ensure` threw "Profile request exceeds the directory budget" for more than 1,024 ids before reading anything. Community admin raised an unhandled rejection and its name search found nobody, and `from:name` search in a channel failed with the same message. (The Members dialog already pages its name reads, #2028.) The directory now names the first 1,024 ids of a larger request (its cache's entry capacity) and leaves the rest unrequested. Fetching more would only evict names the same request had just loaded. Members beyond the cap keep their public-key label, and no notice is shown, because Try again cannot name them. The over-budget request still reaches verified-profile lookup only for the ids that fit (#2220). Two related partial-failure fixes in the same path: - A batch whose response exceeds the reader's 8 MiB budget retries from the same author in halves, like AgentBrowse's catalog pages, instead of loading zero names. A single author still over the budget fails after one read per halving (at most 10 reads for 500 authors). A foreground promotion rejoins whichever part is in flight. - `ensure` reports a failed batch only after its other batches settle, so a caller that tolerates the failure reads every name that loaded. Callers: Community admin catches the optional name read. Message search `from:` proceeds on a partial directory, as the author picker in SearchResults already does. An unnamed member falls through to the relay name search. Not changed: touch-on-read (`get()` for `event()`/`owner()`). Displayed names come from the folded snapshot, not the LRU, so promoting there would not make eviction follow what is on screen. Both functions are also `useSyncExternalStore` snapshot getters (useAgentOwnerEvidence, useProfileOwner) and run inside projection folds, so `get()` would make pure reads reorder the cache. A use-based policy belongs in ensure() with its own eviction tests. Performance: unchanged for requests of up to 1,024 ids whose batches fit the read budget. Request count, sizes and success latency are the same, plus one array slice. A request above 1,024 ids now costs at most three reads (500/500/24) instead of failing, and asking again with those names cached reads nothing. When a batch fails, ensure now rejects once its slowest batch settles rather than at the first failure. The large-roster `from:` test takes about 1 s locally, which this scale contract needs. Docs: docs/channels.md now says a request above the 1,024-entry budget names those that fit instead of failing. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Follow-up to the capacity-cliff review of 2026-10-29 12:02 UTC. The discovery eviction and Members dialog parts of that review were dropped in the rebase onto main: #1574 replaced discovery's 1,024-entry cap with a 256 MiB byte budget and pins the shortened-id fallback, and #2028 pages the dialog's name reads. Review: threads.ts `reactionsCapped` - Dropped it. A full 500-reaction page forced `limited: true`, which ThreadPanel and MediaReviewViewer show as "Thread history limit reached." even while older replies still load. The target still opens, its 500 reactions are a lower bound, and live reactions keep arriving. relay-queries.md no longer says the view is marked `limited`. Review: halving gaps (a) and (b) - Each part is accepted as it lands. A transient failure on part k retries from part k at the halved size instead of from author 0. Kept because the code is no larger than before (the result array and `.then` go away) and is safe: - retryRead runs attempts one at a time with the caller's signal, so two attempts never share the hoisted cursor. - A part that lands after an abort is dropped, as before. - A promotion still rejoins `batch.reading`. Deliberately not changed: - Warm-cache eviction. ByteLru evicts in insertion order and nothing touches an entry on use. A request that loads enough new names therefore evicts the viewer's own profile and on-screen names, and so does a long session once it has seen 1,024 profiles. This predates the branch at every request size. The fix is a use-based policy in ensure(), or reading only the rows that render, as a follow-up. - Halving is still unmeasured against a real 8 MiB response. Performance: - Profile batches that fit the read budget are unchanged: one accept per batch. A halved batch publishes once per part rather than once per batch, so earlier parts' names show one read sooner. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Follow-up covering the review of 2026-10-29 12:53 UTC. The discovery eviction, channel-link pin and Members dialog parts were dropped in the rebase onto main (#1574 replaced the 1,024-entry discovery cap with a 256 MiB byte budget; #2028 pages the dialog's name reads). Review: profile halving - Dropped. The 2 MiB profile cache keeps only about a quarter of an 8 MiB response, so the parts that landed evicted each other, and the case was never measured. Removed the hoisted cursor, the `ReadError` match and their two tests. The batch read is main's again. - Kept: the 1,024-id cap, the Community admin and `from:` search catches, and settling every batch before a failure is reported. - Docs: channels.md says a batch whose response exceeds the 8 MiB read budget (for example heavy legacy inline avatars) still loads no names, and that reading only rendered rows is a follow-up. Review: threads.ts loadMore guard - Reverted to `!snapshot.canLoadMore || snapshot.limited`. With `reactionsCapped` gone it is equivalent, and the thread diff is now only the separate reaction filter. Tombstone read over the request budget: fixed in code - The target's tombstone read sent every overlay id in one `#e` list. The reader rejects filters over 64 KiB (about 975 ids) before sending, so a target with more overlay ids failed to open. One target read can return 499 safety overlays plus 500 reactions, and `remote` can hold more. Carrying only this response's ids would still exceed the budget. It would also stop re-reading tombstones for overlays already in `remote`. So the read now goes in parts of 900 ids: a `for` loop and a slice in place of the `if`, with no new state. Each part keeps the 500-event fail-closed check. Up to 900 ids it is one request, as before. - Test: the full-reaction-page target test now also carries 480 edits (980 overlay ids). It asserts two tombstone requests of 900 and 80 ids covering every overlay, and the newest edit as the content. Before, the reader rejected the request locally and no tombstone query was sent. Performance: - No read is added anywhere that worked before. Tombstone parts only apply above 900 overlay ids, which used to fail. - Profile batches are back to one accept per batch. - thread-target.test.ts signs its 500 reactions and 480 edits once in beforeAll (about 1.45 s); before, 500 were signed inside the test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Fixes the browser-journey CI failure on PR #2114 (run 41377657528). 2d4007a split the exact-target overlay read into a safety-kind filter and a separate `[7]` reaction filter. The policy relay stub answers only the multi-filter batches it recognises, so the new batch fell through to the sidebar-preferences branch and failed `toHaveLength(5)` in every journey that opens a thread (118 tests). The stub now recognises the batch: two filters on the same single `#e` target, each with `limit: 500`, the first carrying exactly kinds 5, 9005, 39005 and 40003 and the second only kind 7. The fixture's `answer()` serves each filter as before. The 1,100-member Members dialog test this commit also timed was dropped in the rebase onto main (#2028 pages the dialog's name reads). Tests (local): - Chromium and WebKit: right-pane-dismissal, message-navigation, inbox, navigation-thread-history, nested-replies, buzz-links, typing, message-actions, message-management, inbox-tabs, inbox-edit and presence specs, 207 of 208 passed in 8.4 min. The one failure is WebKit message-management.spec.mjs:361 (Delete dialog Close not focused), which also fails locally on main. - channel-opening.spec.mjs on chromium-measurements and webkit-measurements: passed. Not run: the full browser suite. Hosted CI will cover it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…lock#2216) ## Problem Opening the members list of a ~1,175-member channel froze the desktop app for minutes. Client metrics from one click recorded main-thread long tasks of 65.5 s, 34.2 s, 15.8 s and 13.6 s in a row: 206 s of long tasks in a 6.5 min session, with 605,689 `fold` calls. The socket starved in the meantime. A foreground read timed out after 23.9 s, the live connection was declared unresponsive and reconnected, and the dialog showed "Relay read timed out" / "The member list could not load". The trace shows 25 full 128-subject kind 30382 batches (1,290 events). Every verified-profile change called `publish()` synchronously inside the socket callback. Each `publish()` refolds every profile, runs the namesake scan and notifies every listener (views then re-render), and nothing yields between frames. block#2209 cut replay notifications to one per batch EOSE. Those were still synchronous, as were every live change, disconnect, reschedule and failure. ## Change `profile-directory.ts`: the verified directory's change callback now schedules one `publish()` per macrotask (`setTimeout`) instead of calling it inline. Changes within one task (a disconnect fan-out, a reschedule, one relay frame), plus frames already queued before the timer fires, coalesce into one publish. This does not strictly cap publishes across separate frames: `live.ts` delivers each WS frame as its own task, so frames arriving after the timer fires get their own publish. `dispose()` cancels a pending publish. Not changed: kind 0 `accept()` and `clear()` still publish synchronously. `session.ts` and `store.ts` treat a profile notification as a synchronous boundary (subscribers can revoke or clear the session inside it), and those paths are already one publish per read or batch. Behavior change: verified overlays reach `snapshot()` one macrotask after the relay frame instead of inside it. Hidden or occluded webviews throttle `setTimeout`, so while the app is backgrounded, verified changes (retractions, the switch to "cached" on disconnect) reach `snapshot()` later. Only readers outside the rendered UI, such as notifications, would see that delay. ## Related - block#2159 makes each publish's namesake scan linear. This PR limits how often publish runs; block#2159 limits what each run costs. - block#2028 limits the members dialog's name reads to the visible page. ## Validation - New test `coalesces a burst of verified changes into one directory notification`: fails on main, passes here. - Verified-profile and ProfilePanel tests now await the macrotask before reading verified state. - Full Vitest run locally: every file that failed under machine load (load average ~30) passed on an isolated rerun. The pre-push hook timed out on two unrelated UI tests (`App.profile`, `ProfileSettings`, 5 s timeouts); both files pass when run alone (49/49). - Not yet run in the desktop app. ## Open before merge - The build that produced the trace above is not recorded, and block#2159 and block#2209 are already in base, so this PR has not yet been shown to fix the freeze. Needed: open the ~1,175-member members dialog with client metrics on base and on head, and record total and max long-task time, `fold` and publish counts, and whether "Relay read timed out" recurs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
Why
Manage members mounted the entire roster and requested more names than the profile directory accepts. On a 2,526-member channel, that means thousands of row trees and a guaranteed name-request failure. AIDA-2332.
What
Member groups show 50 rows per page, retaining full-roster search and counts. Query and role-filter changes reset paging. Live roster changes retain the current page, clamped to the last available page. Subsequent growth retains that clamped page. Names update in place without changing page order or groups. Live roster updates remove departed identities and append arrivals without re-sorting survivors; reopening or refreshing rebuilds the order. Agent counts reflect known classifications at that point and can be partial until refresh. Large rosters appear without waiting for optional names; name reads cover only the displayed pages through the existing bounded directory. Name search combines cached labels with relay search results, including existing members whose names have not loaded and viewers who cannot add members. Public-key search covers the entire roster; DM searches remain local. Public-key labels remain available when names are absent.
Validation
The 2,526-member browser regression mounts 2,526 rows on the base and 50 with this change. Chromium and WebKit exercise Next, focus return, full-roster key search, search reset, Escape, and paging controls at narrow width with enlarged text. A mounted regression holds name reads while paging remains usable, then verifies page-sized requests and recovery. A real-directory regression with 1,100 signed member profiles preserves page-one names and unrelated timeline profiles while paging forward and back. A held-read regression verifies that arriving names preserve visible members and keyboard focus, while explicit refresh picks up newly learned grouping.
Mounted name-search regressions use the verified profile-search endpoint to find an unvisited member, both with and without add-member permission. The view-only case also follows a second search page after a first page of nonmembers. Both cases fail on the preceding implementation and pass with the correction.
A live-roster regression removes members while browsing page two and verifies the page stays selected; shrinking the roster clamps the stored page, and subsequent growth preserves that page and its 50 visible rows. It also covers shrinking below one page before regrowth. The regrowth assertion fails before the persisted clamp and passes afterward. A second regression uses 1,100 named members: removing an unvisited member preserves page-two rows and focus, and re-adding that member also preserves those rows. The same case fails before this fix. All 106 dialog/Solo tests and TypeScript pass on Blox. The page announcement includes “Page” for assistive technology.
The reported permanent native hang has not been reproduced in the isolated fixture.
Screenshots