镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(profiles): reread shown names after an access purge - #2157

Merged
kalvinnchau merged 2 commits into
mainfrom
loganj/profile-cache-refetch
Nov 2, 2026
Merged

kalvinnchau merged 2 commits into
mainfrom
loganj/profile-cache-refetch

Conversation

@loganj

@loganj loganj commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • In the desktop app, people's names often turn back into public keys during a session. They come back only when the view remounts or the app restarts. Linear: BUZZ-501.
  • Cause: when the viewer loses access to any one channel, revokeAccess and denyChannel empty the whole shared profile directory. Losing access includes being removed, a refused channel read, a roster read that leaves out a channel, or a suspended preview. Afterwards only open message windows and threads read their names again. About 30 views read names only when the IDs they show change, so they kept showing keys.
  • Now the directory moves a purge generation on every clear and always notifies. A new hook, useProfileGeneration, exposes it. Each view that ensures the names it shows reruns that read when the generation moves.

Why not reread everything after a purge

The purge is deliberate. Mention completion offers every name in the directory, and revocation.test.ts makes sure that names known only from a lost channel are not read back. Here, a view reads again only the IDs it still shows. revokeAccess defers its notifications until it has purged the lost channel's content, so the first render that sees the new generation also sees the purged IDs.

Details

  • profile-directory.ts: clear() increments generation and always notifies, so a read the purge aborted is retried even when nothing was loaded. ProfileQueries.generation is optional; test fakes need no change.
  • Views covered: channel header avatars, the members dialog, member owners, Solo share (source and destination), Solo people, mention completion and the mention picker, the Inbox and Drafts, Reminders, Watching, Todos, workflow owners, agent activity, huddle avatars and the native huddle window, profile panels, the agent owner, Buzz link previews, composer models, thread and media review authors, search, community admin, agent browse and profile settings.
  • MentionCompletion: its per-session demand budget now resets once per generation. Without that reset, its requested set blocked the reread.
  • CanvasHistory stays mounted read-only after its channel is lost, and its loaded revision authors may be known only from that channel. While read-only, it does not reread on a purge.
  • ProfilePanel: if keyboard focus is on Retry profile when a purge reread starts, Retry stays mounted as pending. On success, focus moves to the profile region, as for an explicit retry.
  • huddle/window.ts (not React): its ensured key now includes the generation.
  • CommunityAdmin: added the missing .catch on its ensure. This existing bug fires more often now that a purge can abort the read.
  • Accepted residual risk: a few views get their IDs from outside access-scoped stores: a profile panel's target, agent owner labels, reminders, watched entries and draft mentions. These views reread right after a purge. A remount already does the same read, so this adds no new exposure.
  • Not in this PR: the directory holds at most 1024 profiles or 2 MB and evicts the oldest inserted entry. A very large member list can still push out names that a view never asks for again.

Testing

  • ChannelMemberAvatars.test.tsx, HuddleAvatarStack.test.tsx: with a real directory, a name is shown, clear() removes it, the view reads it again and it is shown again. The avatar test fails without the change.
  • CanvasHistory.test.tsx: after a purge, writable history rereads its author and read-only history does not. The read-only case fails without the gate.
  • ProfilePanel.fallback.test.tsx: a focused Retry survives a purge reread, and focus moves to the region on success. Fails without the focus fix.
  • huddle/window.test.ts: unchanged participants are read again after a generation change. Fails without the change.
  • profile-details.test.ts: clear() moves the generation and notifies, also when nothing was loaded.
  • Full vitest run on this head: 14794 passed. 3 failed (emoji-mart, EmojiPicker.appearance, StatusEditor); the same 3 fail on unchanged origin/main in the same local worktree setup (symlinked node_modules; Vite denies files outside the root). tsc and biome check pass. Browser specs were not run locally.

🤖 Generated with Claude Code

An access purge empties the whole profile directory. Afterwards only
open message windows and threads read their names again. Views that
ensure names when their IDs change kept showing public keys until
they remounted.

The directory now moves a purge generation on every clear and always
notifies. useProfileGeneration exposes it, and each view that ensures
the names it shows reruns that read when it moves. The purge notifies
after it removes denied content, so only names still on screen are
read again. Canvas history stays mounted read-only after its channel
is lost, so it does not reread while read-only. A focused profile
Retry stays mounted as pending through the reread.

Refs BUZZ-501

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>
@loganj
loganj force-pushed the loganj/profile-cache-refetch branch from 70cb5b1 to ed2f9f0 Compare November 2, 2026 17:15
@loganj
loganj marked this pull request as ready for review November 2, 2026 17:31
@loganj
loganj requested a review from a team as a code owner November 2, 2026 17:31
kalvinnchau

This comment was marked as resolved.

…sage links

The native huddle composer's BroadcastChannel profile proxy had no
generation, so its mention and model pickers never reread after a purge.
The owner snapshot now carries profileGeneration and the client exposes
it. The linked-message byline also rereads its author after a purge.

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

This comment has been minimized.

@kalvinnchau kalvinnchau 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.

🤖 Re-reviewed ce70ea5: the native composer generation and linked-message byline findings are fixed. Recovery checks fail on the previous head and pass here; no new actionable findings. CI is still pending. Native OS and real-relay integration remain unverified.

@kalvinnchau
kalvinnchau merged commit ffa65d8 into main Nov 2, 2026
41 checks passed
@kalvinnchau
kalvinnchau deleted the loganj/profile-cache-refetch branch November 2, 2026 18:13
loganj added a commit that referenced this pull request Nov 2, 2026
## What

When the viewer loses access to a channel, the app now removes that
channel's content but keeps the profile directory (kind 0 names). Before
this change, every access change (deny, suspend, retain omission,
restrict-to-known, a denied channel list) wiped every cached name and
read them again. Names could show as keys after a change to an unrelated
channel.

Follow-up to #2157, which made
views read names again after a wipe. This PR removes the cause of those
wipes.

## Why it is safe

A profile does not depend on channels. The only link is that the app
finds out which keys to look up from the rows in a channel.

- The relay stores kind 0 with no channel (`is_global_only_kind`,
block/buzz `crates/buzz-relay/src/handlers/ingest.rs`). A stray `h` tag
still gets `channel_id = None`.
- Reads match `channel_id IS NULL OR channel_id IN (accessible)`, so a
member who lost every channel still reads every profile in the
community.
- The directory has no channel provenance. The only channel-derived fact
is "the viewer saw this key", and the viewer did see it while they had
access.

A denied channel list follows the same rule. It can mean the community
refused the viewer, but the viewer already saw those names, so keeping
them in memory hides nothing. That path already clears the disk cache. A
403 is also only a guess at "removed": a passing auth failure gets the
same answer, and a wipe there would turn every name back into a key.

Only a full cache clear (logout, community or scope change) empties the
directory.

The same holds for NIP-FI verified profiles (kind 30382). They live in
the same directory (`profiles.clear()` calls `verified.clear()`), so the
old wipe also dropped verified names and stopped their live checks. The
relay signs 30382 with its own key and delivers it on the Global topic
with no channel (block/buzz
`crates/buzz-relay/src/verified_profiles.rs`).

## Changes

- `revokeAccess` (`session.ts`) no longer clears profiles or reads
window and thread names again. It still purges channel content, live
events and confirmed writes for lost channels.
- Profile reads (`{kinds:[0], authors}`) are exempt from revoke
retirement and from the stale-access checks in `readVerified`,
`relayRead`, `readRemote` and `readPage`. Before, a name that was
loading during a revoke was cancelled and never retried, so it stayed a
key. A full cache clear still fences them (`profileReadSurvives` checks
`cacheClearEpoch`), so a pre-clear answer cannot cross the clear.
- `denyChannel` and the denied channel list path (`store.ts`) no longer
clear the directory.
- `eventVisibility` (`event-access.ts`) ignores an `h` tag on kind 0, as
the relay does (`is_global_only_kind`). Before, a profile with a stray
`h` tag was purged from views and dropped from later reads after that
channel's access ended.
- `docs/channels.md`, `docs/browser-testing.md`,
`docs/relay-queries.md`: access loss purges channel content, not
profiles; the window/thread reread and the wipe-and-recover browser
journey are gone.

## Tests

- New: a profile read in flight when another channel's access ends
completes, and the name lands. Fails on main (the read is aborted).
- New: a denied channel list keeps names, and a public profile read in
flight finishes. Fails if that path cancels profile reads.
- New: a full cache clear still fences a profile read whose transport
response already arrived. Fails if the exemption ignores the clear.
- Revoke, deny and "name learned only in the revoked channel" tests now
expect names kept and no extra reads.
- Startup omission and sidebar label tests now expect names kept across
channel removal.
- New: a NIP-FI verified name stays checked, and its live check stays
open, across a denied channel and a denied channel list
(`verified-profile.test.ts`). Fails on main.
- New: a profile with a stray `h` tag stays in views and later reads
after that channel's access ends (`revocation.test.ts`,
`event-access.test.ts`). Fails without the `event-access.ts` change.
- Sidebar label cases keep the empty and failed name reads: a roster
change retries once, then no read loop.
- Browser `dm-labels` journeys (chromium and webkit) now expect the DM
name to survive a channel deletion with no repeated name read, and a
cold name read in flight to finish. Both fail on main.
- The drafts and inbox fixtures no longer depend on the wipe: their held
profiles are not emitted live before release. The inbox fixture now
publishes held profiles on release and does not hold a read open,
because a held-open read takes the one background read slot and blocks
the agent inventory read.

## Not verified

- Old kind 0 rows stored with a `channel_id` from before the global-only
rule. This needs a DB query. If any exist, non-members already cannot
read them today.
- After a restart, saved (on-disk) channel snapshots still drop the
profiles of a lost channel. Those names load again from the relay.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

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>
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