Skip to content

perf(desktop): high-membership channel-switch benchmark - #6455

Open
Maxwellimus wants to merge 27 commits into
mainfrom
perf/switch-tracing
Open

perf(desktop): high-membership channel-switch benchmark#6455
Maxwellimus wants to merge 27 commits into
mainfrom
perf/switch-tracing

Conversation

@Maxwellimus

@Maxwellimus Maxwellimus commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Channel switching on large communities felt 1–2s slow with no way to attribute the time. This PR adds the measurement harness used to diagnose and verify the fixes in the follow-up PRs, plus the two small extractions those fixes build on. It changes no product behavior.

What's here

High-membership benchmark (member-heavy-switch.perf.ts). Run it with:

cd desktop && pnpm build:e2e
pnpm exec playwright test --config=playwright.perf.config.ts member-heavy-switch

It is deliberately outside the CI smoke project. Note that the default
Playwright config does not match *.perf.ts, so invoking it without
--config=playwright.perf.config.ts silently matches zero tests rather than
failing. It measures warm-switch wall time and longtask totals for channel↔channel and channel↔Projects at baseline / 2k / 10k members with fixed message volume. It times click → ready itself (performance.now() around DOM readiness plus a double-rAF), so it is self-contained. An inflateChannelMembers mock-bridge knob (channel name → member count) drives the membership scaling, and a Projects hydration marker lets it gate readiness on the query-dependent surface rather than the shell.

Two extractions the follow-up fixes depend on:

  • useChannelTimelineLoading — the per-channel timeline loading latch, pulled out of ChannelScreen.
  • commitGuardedNavigation — the shared commit flow (skip same-destination no-ops, consult the navigation guard, then navigate), pulled out of useAppNavigation.

One consistency fix: Pulse note actions opened DMs with a raw navigate(), bypassing the navigation guard every other channel entry goes through. They now use goChannel.

Baselines this harness established (pre-fix)

measurement baseline 2,000 members 10,000 members
warm channel↔channel wall, median (4× throttle) 395ms 400ms 515ms
warm channel↔channel longtask total, median 208ms 234ms 321ms
warm channel↔Projects wall, median 142ms 146ms 164ms
warm Projects↔channel wall, median 171ms 198ms 283ms

Every one of the 24 series (4 directions × 2 metrics × 3 member counts) rises
monotonically with member count, with no inversions.

Scope of these numbers. The mock bridge hands the app live JS objects, so
there is no IPC or JSON-parse cost in the measurement — production
get_channels/get_channel_members deserialization also scales with
membership and is not exercised here. These deltas are render-path only, and
are a floor rather than an end-to-end switch cost.

Historical, from a live community during the original investigation (the
instrument that produced them has since been removed, see the scope note):
click→settled paint median 310ms, p90 994ms over n=26; roster refetch on
nearly every switch at 30s staleness.

The follow-up PRs cut the live median to 201ms (−35%) and warm switches to 148ms (−43%).

Scope note

Earlier revisions of this PR carried a live channel-switch tracer: User Timing marks, a [switch-perf] console line, and a JSONL sink behind a Tauri command with log rotation and build-stamped git revisions. The two live-community rows above came from it.

It has been removed. The benchmark that validates the perf work never depended on it, and its steady-state value was a dev console line against a failure mode of reporting a number that never happened — hidden windows, process suspension, superseded switches, and unpainted screens each produced measurements that looked clean and weren't. Keeping it honest cost roughly 1000 lines and a guard at every seam; a wrong perf number is worse than no number, because it sends someone chasing a regression that doesn't exist. The baselines it produced stand on their own and justified the fixes, which are the actual deliverable.

@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

Adversarial-review findings (5 P2s), all addressed in the latest push:

  • Fetch attribution start-guard: fetches attribute to a switch trace only when they started after the switch began (shouldAttributeFetch, unit-tested) — a stale A→B→A first-leg response can no longer claim the trace slot and block the real fetch's attribution.
  • Trace stays attributable through the deferred-commit wait: fetches finishing inside the measured window now land in the record; a newer switch closes the previous record immediately, and a community reset drops it.
  • goChannel boundary: re-selecting the active channel no longer opens a trace that could only time out (history back/forward remains deliberately untraced — documented).
  • Perf sink off the measured path: append_switch_perf_log is now async + spawn_blocking, so filesystem writes never stall the main thread they're measuring.
  • Log rotation: switch-perf.jsonl rotates at 10 MB keeping one prior generation (unit-tested) instead of growing unbounded.

@Maxwellimus
Maxwellimus marked this pull request as ready for review August 21, 2026 21:22
@Maxwellimus
Maxwellimus requested a review from a team as a code owner August 21, 2026 21:22
@Maxwellimus
Maxwellimus requested a review from wesbillman August 21, 2026 21:22
@Maxwellimus
Maxwellimus force-pushed the perf/switch-tracing branch 2 times, most recently from 7f5999b to a5e8e53 Compare August 21, 2026 23:08

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Requesting changes at exact head a5e8e530b9be277691589b616a1264953cc79799 against base d97780b4777f2fe3430b4e30a7d47fc6837ee059.

P1 — Do not record a replaced switch from the replacement trace’s clock.

After switch A reaches settleChannelSwitchTrace() and begins waiting for its deferred render, starting switch B replaces activeTrace. A’s pending callback then calls record() whenever the replacement is non-null, and samples settledAt only at that later callback. A rapid A → B sequence can therefore charge B’s click/main-thread delay to A and emit an A “settled” mark that was never anchored to A’s painted result. This can manufacture the regression the tracer is supposed to diagnose.

Please close or cancel A at replacement time, or retain an A-specific paint completion signal, and add a rapid A → B lifecycle regression for this branch.

P1 — Serialize append and rotation per log path.

Every append runs in an independent spawn_blocking, but metadata → rename → open → append has no per-path serialization. At the 10 MiB boundary, concurrent writers can both decide to rotate; after one renames the file, the other rename fails and its frontend caller silently swallows the error, dropping a trace. Interleavings can also place records into the wrong generation.

Please serialize the complete append-and-rotation transaction and add a concurrent boundary-write test.

@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

Both P1s fixed in the latest push:

P1 — replaced switch recorded from the replacement's clock. settleChannelSwitchTrace now drops the pending record whenever the active trace is no longer the settling trace — whether replaced by a rapid follow-up switch or nulled by a community reset — instead of sampling settledAt from the replacement's timeline. Better no measurement than a fabricated one; recording A against B's clock could manufacture exactly the regression this tracer exists to diagnose. Covered by three new lifecycle tests driving the rAF chain with a stubbed frame queue (channelSwitchPerf.test.mjs): rapid A→B drops A and still records B; reset drops the record; an undisturbed settle records exactly once.

P1 — unserialized append/rotation. append_line_rotating now takes a global mutex around the whole metadata→rename→append transaction (one lock is sufficient: the app writes a single log path — noted in the doc comment). New concurrent boundary test: 8 writers × 4 lines against a cap sized to cross the rotation boundary exactly once; every line must survive into the live file or the single rotated generation. Red before the mutex, green after.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Re-reviewing exact head 99b0a590035a89f72a1942a48c32b0f4279cc996 against target main@f99532585a0715bac73b4a6361a9b4966bdb5095 (PR-recorded base 0e48ff26915aa32d5f05208847b9aba75f4f19cd; merge base d97780b4777f2fe3430b4e30a7d47fc6837ee059). The previous rapid-switch clock-theft and same-process append race are fixed. The new lifecycle tests and global lock cover those cases. The remaining blockers are:

P1: replace the retained generation portably before rotating. desktop/src-tauri/src/commands/perf_log.rs:70-75 renames the live log directly onto an existing .1. On Windows, rename does not replace the destination. After the first retained generation exists, the next rollover errors before opening/appending the live file, and channelSwitchPerf.ts:112-125 intentionally swallows that error, so subsequent traces disappear without a signal. This repository already documents and tests the same platform rule in managed_agents/storage.rs:663-684 and storage_tests.rs:751-778. Remove the old .1 while holding PERF_LOG_LOCK, then rename, and add a regression that seeds both current and .1 before rotation.

P1: do not settle while the initial deferred timeline is still unpainted. MessageTimeline.tsx:265-305 knows when the deferred snapshot is stale, but its data-render-pending marker exists only inside the message-list branches (:754-760, :845-855). During the initial empty/channel-changed to loaded transition, the skeleton renders and there is no marker. channelSwitchPerf.ts:284-293 therefore sees “not pending” and records after the next frame even though the heavy deferred list may not have committed or painted. Put the readiness signal on an always-mounted channel-scoped wrapper (or expose an explicit commit signal), and cover a real empty-to-loaded browser switch that asserts the emitted CHANNEL_SWITCH_MEASURE lands after the deferred list.

P1: cancel a channel trace when navigation leaves the channel surface. Only another goChannel call replaces the singleton. goProjects, goHome, and the other non-channel navigations do not clear it, and useChannelSwitchTraceMarks.ts has no route-exit cleanup. Repro: begin A, leave for Projects before A settles, then use history back within 30 seconds. History bypasses goChannel, the returning A screen matches the stale trace, and time spent on Projects is recorded as A’s switch latency. Add route-exit cancellation plus a channel → non-channel → history-back regression.

P2: reset before asynchronous community teardown, and attribute only accepted fetches. resetCommunityState() does not clear the trace until after await resetNavigationDeepLinkDrain() (useCommunityInit.ts:59-83), so queued frame callbacks can record during teardown; rejection skips the reset entirely. Clear it before the first await. Separately, messages/hooks.ts:275-289 attributes the window fetch before reconcileFetchedChannelWindow() executes signal.throwIfAborted(). A canceled request can win the singleton windowFetch ??= slot and block the accepted replacement. Attribute only after the abort gate/reconciliation succeeds, with canceled-first/accepted-second coverage.

The PR is also currently conflicted with main, specifically the post-#6456 roster-freshness work in hooks.ts, and GitHub exposes only DCO for this head. Rebase, preserve the 5-minute roster policy while retaining attribution, rerun the perf harness against current main, and run the normal checks after resolving the code findings above.

@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

All five findings from the re-review addressed in the latest push:

P1 — portable rotation over an existing .1: the retained generation is now removed (under PERF_LOG_LOCK) before the rename, matching the platform rule documented in managed_agents::storage::start_install_log_session. New regression seeds both the live file and .1 before forcing a rollover.

P1 — settle while the deferred timeline is unpainted: the data-render-pending marker moved to the timeline's always-mounted wrapper (the message-list branches' own markers are removed as redundant), so the tracer's settle-wait observes the skeleton→loaded transition too. New browser regression (switch-settle-after-paint.spec.ts): a real cold empty→loaded switch into a 600-message channel polls for CHANNEL_SWITCH_MEASURE inside the page and — in the same evaluation turn — asserts rows are painted and no deferred commit is pending at the moment the measure exists.

P1 — trace canceled on leaving the channel surface: useChannelSwitchTraceMarks adds a channel-id-keyed unmount cleanup that abandons the trace, covering goProjects/goHome/every non-goChannel exit. On A→B switches the cleanup runs with A's id after B's begin, so it only ever abandons its own trace. New lifecycle regression: begin → route-exit abandon → history-back settle records nothing.

P2 — reset ordering: resetChannelSwitchTrace() now runs before the first await in resetCommunityState(), so queued frame callbacks can't record during async teardown and a rejected drain can't skip the reset.

P2 — attribute only accepted fetches: the window fetch is attributed after reconcileFetchedChannelWindow() (whose abort gate throws first); duration still measures the fetch alone. New canceled-first/accepted-second regression asserts the accepted fetch owns the windowFetch slot in the emitted measure.

The conflict noted in the review was already resolved (branch rebased onto current main, preserving the post-#6456 roster-freshness work — CHANNEL_MEMBERS_STALE_TIME_MS intact with trace attribution layered on top). Full suite green: 5,410 unit / Rust 6 perf-log tests / smoke including the two new regressions.

@Maxwellimus
Maxwellimus requested a review from wesbillman August 22, 2026 17:25
tlongwell-block added a commit that referenced this pull request Aug 24, 2026
… sends (#6572)

## Summary

Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.

**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).

Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.

**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.

**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.

**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).

### Related issue
Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.

### Review follow-ups
Addressing Carl's reviews
[5001114109](#6572 (review))
and
[5002596542](#6572 (review)),
each pushed as new commits (no rebase):

- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.

### Testing
At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).

Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a
comments-only commit correcting two `profile/hooks.ts` freshness
comments from 60s to 10 min; pre-push desktop check/typecheck/test
5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7`
at PR open, since moved to `4baccd539` (#6558, mobile only — zero file
overlap, `git merge-tree` clean):

- `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level
call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount
then 1 on invalidate with wholesale replacement)
- Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` +
`channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at
`7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec
persists a head, reloads into a fresh mock relay with the head fetch
held 5s, asserts the persisted row paints within 2s, exactly one
`get_channel_window` after open, and the stale row is removed when the
authoritative page lands.
- `pnpm typecheck`, `pnpm check` — clean

At `7acbf951b` (everything except the two-line `useThreadReplies.ts`
staleTime revert and the test-only `relay-reconnect.spec.ts` change),
also green in one shell:
- `just desktop-tauri-test` — 2,859 passed / 0 failed across the
workspace (channel_head_cache: wire shape, LRU+caps, schema reset,
corrupt-row skip)
- `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p
buzz-relay --lib` — 908 passed / 0 failed
- `just check` components: fmt-check, clippy, desktop-check,
desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy,
web-check, mobile-check, file-size-check — all green
- `just desktop-build`, `web-build`, `desktop-tauri-check`,
`mobile-test` (1,661 passed) — all green

CI note: the "Unit Tests" job goes red on this PR and on `main` whenever
it hits a poisoned `rust-cache` entry (an empty-directory skeleton of
`target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts),
surfacing as `could not find native static library sherpa-onnx-c-api` in
`buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry
and rerunning turned the job green at `0c492366d` (28/28); it re-poisons
on the next `main` push until the workflow clears that directory after
cache restore.

Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and
line-by-line by me before opening; the staleTime fix re-verified by Wren
and me independently; the relay-reconnect test fix bisected and verified
by me.

---------

Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
klopez4212 pushed a commit that referenced this pull request Aug 24, 2026
… sends (#6572)

Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.

**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).

Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.

**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.

**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.

**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).

Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.

Addressing Carl's reviews
[5001114109](#6572 (review))
and
[5002596542](#6572 (review)),
each pushed as new commits (no rebase):

- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.

At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).

Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70 + a
comments-only commit correcting two `profile/hooks.ts` freshness
comments from 60s to 10 min; pre-push desktop check/typecheck/test
5,387/0 re-ran at 0c49236) in one shell; `origin/main` = `040b203f7`
at PR open, since moved to `4baccd539` (#6558, mobile only — zero file
overlap, `git merge-tree` clean):

- `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level
call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount
then 1 on invalidate with wholesale replacement)
- Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` +
`channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at
`7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec
persists a head, reloads into a fresh mock relay with the head fetch
held 5s, asserts the persisted row paints within 2s, exactly one
`get_channel_window` after open, and the stale row is removed when the
authoritative page lands.
- `pnpm typecheck`, `pnpm check` — clean

At `7acbf951b` (everything except the two-line `useThreadReplies.ts`
staleTime revert and the test-only `relay-reconnect.spec.ts` change),
also green in one shell:
- `just desktop-tauri-test` — 2,859 passed / 0 failed across the
workspace (channel_head_cache: wire shape, LRU+caps, schema reset,
corrupt-row skip)
- `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p
buzz-relay --lib` — 908 passed / 0 failed
- `just check` components: fmt-check, clippy, desktop-check,
desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy,
web-check, mobile-check, file-size-check — all green
- `just desktop-build`, `web-build`, `desktop-tauri-check`,
`mobile-test` (1,661 passed) — all green

CI note: the "Unit Tests" job goes red on this PR and on `main` whenever
it hits a poisoned `rust-cache` entry (an empty-directory skeleton of
`target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts),
surfacing as `could not find native static library sherpa-onnx-c-api` in
`buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry
and rerunning turned the job green at `0c492366d` (28/28); it re-poisons
on the next `main` push until the workflow clears that directory after
cache restore.

Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and
line-by-line by me before opening; the staleTime fix re-verified by Wren
and me independently; the relay-reconnect test fix bisected and verified
by me.

---------

Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Requesting changes at exact head 2a4d128a40b41c804f3c13966e60a08c4f0a8cad for three remaining instrumentation-correctness issues:

  1. [P1] Do not settle while the lazy channel UI is still suspended. ChannelScreen drives settleChannelSwitchTrace from query readiness, but ChannelPane may still be behind ChannelScreenLoadingFallback. The pending marker exists only inside MessageTimeline, so the tracer interprets an absent marker as ready. In an adversarial browser run that delayed the real ChannelPane chunk, a deep-history measure landed at 331.7 ms with zero rows while the fallback was still visible; the rows mounted only after releasing the chunk. Make readiness explicit from the mounted timeline or mark the outer fallback pending, and add delayed-chunk coverage. The checked-in localhost smoke test does not exercise this interval.

  2. [P1] Preserve traces through React StrictMode's effect replay. The new route-exit cleanup in useChannelSwitchTraceMarks abandons the just-opened trace during StrictMode's development-only mount cleanup, then the effect remount does not recreate it. This breaks the PR's stated dev-build/Performance-panel workflow. Reproduced against the real Vite dev runtime: one buzz:channel-switch:start mark, zero click-to-settled measures after rows painted, and switch-settle-after-paint.spec.ts timed out waiting for a settle. The production-style E2E build passes because it does not run StrictMode effect replay. Route-exit cancellation needs to distinguish an actual route/channel exit from effect replay, with dev-runtime coverage.

  3. [P2] Enforce the 4 KiB cap after backend metadata is added. shape_perf_log_line checks only record_json.len(), then appends the unbounded BUZZ_PERF_LOG_LABEL. A 13-byte record plus a 1 MiB label serializes to a 1,048,615-byte JSONL line, defeating the documented defensive cap and inflating always-on telemetry I/O and retention. Bound/reject/truncate the label or validate the final serialized line, and cover it.

The prior Windows rotation, concurrent rotation, rapid replacement, deferred timeline marker, route history, pre-await teardown, and canceled-fetch attribution findings are otherwise fixed. Focused JS tests passed (23/23), git diff --check passed, and exact-head CI is green. Local Rust tests could not start because this machine lacks cmake; exact-head CI includes successful Rust and Windows jobs.

Non-code merge note: GitHub reports the PR conflicted with current main. git merge-tree finds one keep-both conflict in projectChannelWindow.test.mjs; preserve both independently appended regression tests when rebasing.

Maxwellimus added a commit that referenced this pull request Aug 24, 2026
Address the three findings from the 2026-08-24 review of #6455:

- The lazy ChannelPane's Suspense fallback now carries the
  data-render-pending marker, so a switch trace can no longer settle
  while the pane chunk is suspended. New Playwright regression holds
  the chunk and asserts no measure lands until rows paint.
- Route-exit trace abandonment is deferred one microtask and canceled
  by the effect re-setup, so StrictMode's dev-only effect replay no
  longer kills a just-opened trace. New jsdom regressions run the real
  react-dom dev replay; the settle spec also passes against the Vite
  dev runtime.
- The perf-log 4 KiB cap is enforced on the final serialized line, and
  BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary, so
  a runaway label can neither inflate the sink nor kill every record.

Also extracts the timeline-loading latch + trace marks from
ChannelScreen into useChannelTimelineLoading (file-size ratchet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

All three findings from the latest review addressed in e50edba, and the branch is rebased onto current main — both independently added projectChannelWindow.test.mjs regression tests preserved; the PR is mergeable again.

P1 — settle while the lazy channel UI is suspended: the ChannelPane Suspense fallback (ChannelScreenLoadingFallback) now carries data-render-pending="true" (via a layout-transparent contents wrapper), so the tracer's settle-wait observes the suspended interval the same way it observes the deferred timeline commit. New regression in switch-settle-after-paint.spec.ts intercepts and holds the ChannelPane-*.js chunk: no measure may land while it is held, and after release the settle must land with rows painted. Red before the marker, green after.

P1 — StrictMode effect replay kills the trace: route-exit abandonment is now scheduled one microtask out (scheduleRouteExitAbandon) and canceled by the effect re-setup (cancelRouteExitAbandon). StrictMode's replay runs cleanup + re-setup synchronously within one commit, so the re-setup cancels the abandon before its microtask fires; a real route exit has no re-setup, so the abandon still runs — and before any frame callback, so a queued settle can't record in the gap. New useChannelSwitchTraceMarks.test.mjs renders the hook under the real react-dom dev build in StrictMode (trace survives replay and settles exactly once; a real unmount still abandons; an A→B switch's deferred abandon of A never kills B's trace) — the first test fails against the old synchronous abandon. Also verified live: the settle spec passes against the Vite dev-server runtime, the environment the review reproduced the timeout in.

P2 — 4 KiB cap defeated by metadata: BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary (truncation rather than rejection, so a fat-fingered label can't silently drop every record for the whole run), and the cap is re-enforced on the final serialized line after the gitSha/label fold-in. Three new unit tests cover the 1 MiB label, multibyte boundary truncation, and a record that outgrows the cap only after metadata.

Housekeeping: the timeline-loading latch + trace marks moved verbatim from ChannelScreen into a new useChannelTimelineLoading hook — the rebase left ChannelScreen.tsx over the repo file-size ratchet, which forbids growth.

Validation: Rust perf_log tests 9/9 (fmt + clippy clean); desktop unit suite 5450/5450; settle spec 2/2 on the production e2e build and passing on the dev runtime; the data-render-pending-sensitive smoke specs (mentions, send-channel-binding, markdown-parse-cache) 76/76.

Maxwellimus and others added 17 commits August 26, 2026 12:34
Address the three findings from the 2026-08-24 review of #6455:

- The lazy ChannelPane's Suspense fallback now carries the
  data-render-pending marker, so a switch trace can no longer settle
  while the pane chunk is suspended. New Playwright regression holds
  the chunk and asserts no measure lands until rows paint.
- Route-exit trace abandonment is deferred one microtask and canceled
  by the effect re-setup, so StrictMode's dev-only effect replay no
  longer kills a just-opened trace. New jsdom regressions run the real
  react-dom dev replay; the settle spec also passes against the Vite
  dev runtime.
- The perf-log 4 KiB cap is enforced on the final serialized line, and
  BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary, so
  a runaway label can neither inflate the sink nor kill every record.

Also extracts the timeline-loading latch + trace marks from
ChannelScreen into useChannelTimelineLoading (file-size ratchet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…nchmark

Address the three findings from the 2026-08-24 re-review of #6455, plus
one finding from a follow-up adversarial review:

- The switch trace now opens inside commitGuardedNavigation, only after
  the navigation guard accepts — a refused click can no longer leave an
  orphan trace that a later history navigation would settle with
  inflated time. The commit flow is extracted and dependency-injected;
  new unit tests pin guard ordering and the refusal→history-back case.
- The perf-log 4 KiB cap now bounds bytes on disk: one byte is reserved
  for the newline writeln! appends. New boundary test measures the file.
- The channel↔Projects benchmark gates readiness on a new
  data-projects-hydrating marker (driven by the surface's query fan via
  isLoading, so disabled queries never wedge it) instead of the shell
  header, verifies member inflation on both inflated channels, uses a
  prefixed ready-selector for general, and reports each switch direction
  as its own median so a one-leg regression cannot hide.
- A settle wait that hits its 5s deadline while the render is still
  pending now records with settleWaitTruncated instead of posing as an
  honest settled paint — the >deadline tail is what the tracer exists
  to expose. Also removes the dead ChannelSwitchFetchTrace type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…esty

Blind-review round 2 (Opus) findings:

- P1: rAF suspends in hidden windows, so a queued settle fired only on
  the user's return and charged the whole absence to the switch as a
  clean record (reproduced at runtime). Settle now drops when the window
  is already hidden, poisons the wait on visibilitychange, and drops any
  trace older than the entry timeout plus the render wait — nothing
  legitimate can reach that age.
- The JSONL append is a single write_all: writeln! issues two write
  syscalls and the lock is process-local while the log path is not, so a
  second process sharing the log dir could interleave mid-line.
- The User Timing buffer keeps only the latest switch's mark/measure —
  desktop sessions run for weeks and the buffer is never GC'd.
- The route-commit mark moved to a layout effect so it stamps commit
  time, not first-paint time.
- Comment honesty: the member-heavy harness now states that mock-mode
  IPC hands live objects (parse cost not exercised) and that the
  Projects hydration marker guards cold/invalidated samples only; the
  goChannel trace-anchor comment describes what the same-channel guard
  actually skips; the Rust concurrency test's byte arithmetic corrected.
- Longtask sampling drains PerformanceObserver.takeRecords() at sample
  time so a longtask ending just before the resolve frame isn't lost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 3 (Sonnet) finding: beginChannelSwitchTrace marked
unconditionally but only record() cleared, so every abandoned/dropped
trace (forum visits, route exits, hidden-window and frame-starved
drops) leaked a permanent buzz:channel-switch:start entry. Clearing
the previous start mark at begin bounds the buffer to one entry no
matter how the prior trace ended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…uffer at begin

Blind-review round 4 (Opus xhigh) findings:

- A not-pending frame landing past the wait deadline is rAF starvation
  (system suspend, App Nap — no visibilitychange), not render time: a
  fast settle followed by a stall recorded up to 34s as a clean switch
  under the click-anchored 35s age guard. Such frames now drop.
- Visibility accounting now spans the whole trace: one module-level
  visibilitychange watcher timestamps transitions, and any transition
  since the click (not just during the settle wait) drops the trace —
  a window minimized while the fetch was in flight no longer records
  its absence as switch time. Replaces the per-settle listener.
- begin() now clears the previous switch's settled mark and measure
  too, so a consumer polling the User Timing buffer mid-switch can
  never read the prior switch's entries as the current one's.
- The goChannel comment no longer claims every channel navigation
  funnels through it: Pulse startDm navigates to the channel route
  directly and is untraced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 5 (Fable medium) finding: a longtask trailing switch
N is delivered by the PerformanceObserver in a later task and landed in
switch N+1's freshly reset array, inflating its longtask lines. Drain
takeRecords() and discard before each sample's reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 6 (Sonnet xhigh) finding: a transient lock on the
rotated generation (AV/EDR or an editor, chiefly Windows) failed the
whole append — and since the live file stays oversized, every later
append re-entered the same failing branch, silently dropping every
record until the lock cleared. Rotation is now best-effort: on failure
the line appends unrotated and the size cap re-applies once a later
rotation succeeds. Unix regression locks the directory and asserts the
line survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 7 (Opus medium) findings:

- P2: a trace begun while the channel route was still resolving never
  mounted ChannelScreen, so no route-exit cleanup existed; leaving for
  Home and history-backing into the channel within 30s settled the
  stale trace with the time spent away. Any committed non-channel
  navigation and any history traversal now drop the active trace at the
  navigation layer (dropActiveChannelSwitchTrace), which covers traces
  no component ever owned. Same-channel navigations keep the live
  trace.
- The delayed-chunk spec now asserts no CLEAN measure while the chunk
  is held (the tracer honestly emits a truncated one if its 5s deadline
  passes) and guards its own timing budget explicitly so a slow CI box
  fails with the real reason.
- The rotation-degradation test blocks rotation with a non-empty
  directory instead of chmod, so it also holds when tests run as root
  (containers), and now runs on Windows too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…op starved frames

Blind-review round 9 (Opus high) findings:

- P2: the members queryFn attributed unconditionally, so a roster fetch
  superseded by a live join/leave invalidation could claim the trace's
  one-shot membersFetch slot with a stale count and duration. It now
  checks signal.throwIfAborted() before attributing — same rule as the
  window fetch's reconcile abort gate.
- P2: a permanently dead sink (unwritable log dir, stale directory at
  the log path) was indistinguishable from no traced switches: console
  lines kept flowing while every append failed into .catch(() => {}).
  The first persistence failure now warns once.
- P3: during a suspension that fires no visibilitychange, the deferred
  render-pending marker stays latched, so the settle wait recorded a
  truncated measure inflated by the whole stall (bounded only by the
  35s age guard). A single inter-frame gap beyond 3s — beyond any real
  main-thread stall — now drops the sample.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…le specs

Blind-review round 10 (Fable high) finding: the specs' settle polls
accepted a legitimately truncated measure — the tracer honestly hitting
its 5s render-wait deadline on a slow box — and then failed the painted-
rows assertion with a message blaming the tracer. Both tests now poll
for clean measures only and fail truncated-only runs with an explicit
harness-timing message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
… guard

Blind-review round 12 (Opus xhigh) findings, both reproduced:

- P1: the round-9 abort gate destructured { signal } in the members
  queryFn — merely reading that getter sets React Query's
  abortSignalConsumed, switching the roster query to cancel-and-revert
  when its last observer unsubscribes mid-fetch. Interrupted switches
  discarded rosters that previously landed in cache (the Tauri call
  cannot be cancelled, so the work was paid and thrown away), and
  A->B->A warm switches repaid a full roster fetch. Replaced with a
  per-channel supersession token (openChannelMembersFetch) checked by
  traceChannelMembersFetch — stale fetches stay out of the one-shot
  slot without touching the signal. Sequences reset with community
  state.
- P2: lastFrameAt started null, so the settle-entry -> first-frame
  window skipped the starvation guard: a no-visibilitychange suspension
  there recorded a truncated measure inflated by the whole stall
  (bounded only by the 35s age guard). The gap clock is now seeded at
  settle entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 13 (Sonnet xhigh) hardening: leavesChannelSurface
used a /channels/ prefix check, so sibling routes (forum posts) counted
as staying on the channel surface and kept a live trace alive. No
currently reachable path turns that into a wrong record — every
re-entry overwrites or drops the trace — but the invariant from
9b46232 should hold structurally, not incidentally. Anything other
than the exact message-view route now drops the active trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…ent test

Blind-review round 14 (Opus medium) P3s:

- settleChannelSwitchTrace guarded window but then read document
  (visibilityState, querySelector) unguarded — the document guard in
  ensureVisibilityWatcher was dead protection. The settle entry gate now
  covers both globals.
- The concurrent boundary test folded the rotated generation in behind
  if-let, so a regression where rotation never fires under contention
  would pass green with all 32 lines in the live file. The read is now
  unconditional, and the boundary comment's arithmetic is corrected
  (rotation triggers before the 23rd append).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 15 (Fable xhigh) finding: a clean measure recorded
behind the held chunk — the exact regression under test — clears the
start mark and nulls elapsedSinceClick, so the timing-budget assertion
failed first with a 'rerun, not a tracer bug' message stating the
opposite of the truth. The contract assertion now runs first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
…budget guard

Blind-review round 16 (Opus high) P3s:

- A same-task unmount-then-renavigate to the same channel left the exit
  cleanup's scheduled abandon pending, and its microtask killed the
  freshly opened trace (silent lost sample). beginChannelSwitchTrace
  now revokes any pending abandon for its channel.
- The delayed-chunk spec's timing-budget guard hit expect(null) with a
  raw matcher error when a truncated record had already cleared the
  start mark — the one case its message exists for. Boolean form now
  covers the null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
… dispatch

Blind-review round 17 (Fable high) finding: startedAt sampled
performance.now() at click-handler dispatch, silently excluding input
delay — a click queued behind a long task under-reported by the whole
queueing time, unbounded, in exactly the contention regime the tracer
exists to expose. begin() now anchors at the dispatching event's
timeStamp when one is present (window.event is set only during
synchronous dispatch, so stale timestamps cannot leak in from async
continuations; min() guards skewed clocks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 18 (Opus xhigh) finding: 26ee898 moved the
measure's origin to the input event's timeStamp but left the start mark
at handler-dispatch time, so the Performance panel showed the measure
beginning before its own start mark by the input delay — two different
switch durations from one instrument. The mark now carries
startTime: startedAt, pinned by asserting mark and measure share the
anchor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Requesting changes at exact head a0a7989022c56e98ab4f668e95707fe4d6556235 against base de188ebf06f701ca0f47c51f859da21e19a35f04.

P1 — Guard the final paint frame against suspension before recording.

settleChannelSwitchTrace() applies the age, frame-gap, pending-render, and visibility guards in awaitDeferredCommit, but after that frame declares the render ready it queues one more requestAnimationFrame and calls record() without checking elapsed age or the gap from the prior frame (desktop/src/shared/lib/channelSwitchPerf.ts:511-535). App Nap/system suspension can occur in exactly that interval without a visibilitychange, so the resumed final frame records the entire absence as an ordinary clean switch. I reproduced this deterministically at this head: readiness at t=10ms followed by the final frame at t=20,000ms emitted [switch-perf] ... total=20000ms with no settleWaitTruncated flag. The preceding frame-starvation tests stop before this final-frame seam. Carry the prior-frame timestamp into the final callback and reapply the gap/age/visibility decision there, with a regression that stalls specifically between readiness and record.

P1 — Either include DM-opening latency in the advertised click-to-settled metric, or narrow the contract and label honestly.

The trace starts only when goChannel runs (desktop/src/app/navigation/useAppNavigation.ts:263-340), but first-class DM actions await openDm before calling it, including profile messaging (desktop/src/features/profile/ui/useProfileInteractionActions.ts:150-158), channel profiles (desktop/src/features/channels/ui/useChannelProfilePanel.ts:60-65), and Home (desktop/src/features/home/ui/HomeView.tsx:222-226). That relay/open-DM delay is user-felt click latency but is excluded. Pulse note actions go further: after the same await they use raw navigate, so that DM channel entry is wholly untraced (desktop/src/features/pulse/lib/useNoteActions.ts:155-164). This contradicts the PR's “every channel navigation records click → route commit → settled paint” contract and biases the baseline away from a first-class surface. Start an interaction trace at the DM action and carry it safely through open/guard/commit, or explicitly rename and document this as a narrower goChannel-dispatch metric with an inventory of excluded entry paths. Add delayed-open_dm workflow coverage and Pulse parity.

P2 — Cancel only the trace opened by a route navigation when navigate() rejects.

commitGuardedNavigation begins the trace after the guard accepts, then directly awaits navigate() (desktop/src/app/navigation/commitGuardedNavigation.ts:51-60). A loader/router rejection leaves that trace active despite no committed destination. An untraced re-entry to the target within 30 seconds can then settle the failed attempt plus intervening time. Existing tests cover guard refusal, no-op, and successful navigation, but not a rejected commit. Return an opaque trace identity from begin and cancel that exact identity in catch before rethrowing; channel-only cancellation could erase a newer same-channel attempt. Pin rejection followed by a later settle.

The app's single-instance guard makes the sink's process-local rotation lock acceptable for the supported runtime topology; I am not treating a hypothetical unsupported concurrent-instance rotation as a blocker. The sink's byte bounds, JSON shaping, failure warning, and same-process serialization otherwise held under inspection.

Three review findings against the tracer's honesty contract:

- The readiness -> paint frame was unguarded. `awaitDeferredCommit` checks
  age, frame gap, and visibility on every frame it drives, but the one rAF
  it queues before `record()` checked neither age nor gap, so a suspension
  in that seam (App Nap fires no visibilitychange) recorded the whole
  absence as a clean switch.
- DM actions await `open_dm` before `goChannel`, so that relay round-trip
  sat outside the measurement; Pulse's note actions used raw `navigate` and
  produced no measurement at all. Callers now capture the click anchor and
  hand it to `goChannel`, and Pulse routes through `goChannel`.
- A `navigate()` rejection left the trace it opened active, so an untraced
  re-entry could settle the failed attempt. Cancellation is by trace
  identity, so a newer same-channel attempt is never erased.

Signed-off-by: Max Lampert <maxwell@squareup.com>
Cycle-1 adversarial review findings.

- The starvation heartbeat rescheduled forever for a trace that was never
  settled, dropped, or replaced. It hung the unit suite (a test stubs rAF as
  setTimeout, so the timer kept node's loop alive) and leaked a frame callback
  per frame in production. Bounded to the trace's own liveness budget.
- `routeStillOnChannel` read `location.pathname`, but the app uses hash
  history, so it answered false for every real channel route and degraded
  route-exit handling to an unconditional abandon.
- Supersession and community reset discarded traces with no record, censoring
  the impatient-second-click case — precisely the slow switches worth seeing.
  `community-reset` was a declared reason that nothing emitted.
- Drop accounting had no test coverage at all; deleting it left the suite
  green. Covered now, verified by reverting each guard.
- `settleChannelSwitchTrace` was not idempotent: a second call re-stamped the
  fetch-attribution bound and spawned a duplicate wait chain.
- `anchorDiscarded` reached the console but never the record it documents.
- NewMessageScreen back-dated the anchor across the message publish, so send
  latency was reported as switch latency.
- The canceled-fetch test restated the queryFn's reconcile-then-attribute
  ordering instead of exercising it; inverting production left it green. The
  ordering now lives in `reconcileAndAttributeChannelWindow`, which the test
  drives directly.

Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was accumulating defects faster than it was answering
questions. Nearly every review finding lived in a seam that existed only
because the tracer inferred more than it could observe: a starvation
heartbeat, a per-seam guard matrix, DOM-attribute sniffing with a commit
generation, click anchors threaded by hand through nine async call sites,
and an on-disk JSONL sink with rotation and byte caps. Each fix added a
seam, and the next review found the guard that seam was missing.

The perf work these numbers support matters more than the numbers, so the
instrument is now scoped to what it can measure without inference:

- click (or navigation) -> route commit -> settled paint, as a console line
  plus User Timing marks/measures. That is what the Performance panel and
  the perf harness read.
- the message-window fetch when it starts inside that interval.
- one drop rule per unmeasurable condition (hidden window, timeout,
  superseded, left the surface), each printing why.

Removed: the Rust perf-log sink and its git-revision baking, the click-anchor
threading and every call site that existed only to carry it, the starvation
heartbeat, the roster-fetch attribution and its sequence machinery, and the
commit-generation burst gate.

Scope bounds are now stated in the module doc rather than inferred: callers
that await before navigating are measured from the navigation, a bounded
frame wait replaces the 5s render deadline, and a sample past that bound is
flagged rather than dropped.

Net: 2231 lines removed, 462 added.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was over half this PR and the source of nearly every review
finding, and the benchmark that actually validates the perf work does not
depend on it: member-heavy-switch.perf.ts times click -> ready itself with
performance.now() and DOM readiness, and never reads a tracer export.

Its remaining value was a dev console line, against a failure mode of
reporting a number that never happened — which is worse than reporting
nothing, because it sends someone chasing a regression that does not exist.
The live baselines it produced are already recorded in the PR description
and stand on their own; they justified the fixes, and the fixes are the
deliverable.

Removed: channelSwitchPerf.ts and its tests, useChannelSwitchTraceMarks,
switch-settle-after-paint.spec.ts, the fetch attribution in the window
queryFn, the trace hooks in commitGuardedNavigation/useAppNavigation, and
the community-init reset.

Kept, because the perf fixes build on them:
- member-heavy-switch.perf.ts plus the inflateChannelMembers bridge knob —
  the high-membership benchmark
- useChannelTimelineLoading — the per-channel loading latch
- commitGuardedNavigation — the shared guard/no-op commit flow
- the Projects hydration marker the harness gates readiness on
- routing Pulse note actions through goChannel, so that DM entry goes
  through the same navigation guard as every other one

Signed-off-by: Max Lampert <maxwell@squareup.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus Maxwellimus changed the title perf(desktop): channel-switch tracing + high-membership perf harness perf(desktop): high-membership channel-switch benchmark Aug 27, 2026
Blind review found the guard-refusal branch untested, and a refutation pass
disagreed. The refutation tested the wrong mutation: DELETING the
`allow()` call fails three ordering tests, so it looked covered. Consulting
the guard and discarding its answer passed all ten.

That is the branch that stops a channel switch while a thread edit is
unsaved, so a regression there would have shipped silently. Two tests now
pin it — refused, and refused-while-forced — and the ignore-the-answer
mutation fails both.

Also removes residue the same review found in the tracer teardown:
- a stranded `append_switch_perf_log` arm in the e2e mock bridge, for a
  Tauri command deleted two commits ago
- MessageTimeline's marker comment, which justified its placement by a
  tracer that no longer exists; its readers are now the perf benchmark and
  the e2e readiness specs
- ChannelScreenLoadingFallback's `data-render-pending` wrapper, added
  solely for the tracer. Measured benchmark switches are warm, so the lazy
  pane never suspends there and nothing asserts the marker. Reverted to
  main rather than left as an unowned production DOM node.

Signed-off-by: Max Lampert <maxwell@squareup.com>
…'t need it

The render-pending marker was moved onto MessageTimeline's always-mounted
wrapper for the switch tracer, which is gone. Tested whether the surviving
benchmark still needs it: reverted the file to main, rebuilt, re-ran all
three scenarios. Medians are unchanged within run-to-run noise —

  general->deep-history   395.3 -> 394.3 baseline, 515.4 -> 520.4 at 10k
  deep-history->general   173.3 -> 179.3 baseline, 314.9 -> 321.4 at 10k
  general->projects       142.1 -> 141.7 baseline, 164.4 -> 169.8 at 10k
  projects->general       171.0 -> 179.6 baseline, 282.8 -> 290.5 at 10k

— and membership scaling stays monotonic in every direction. The move was
not load-bearing.

Reverting it removes this PR's last write to the document-wide
data-render-pending namespace that review flagged as the one surviving piece
of the tracer's shape, and it removes an untestable risk: release-smoke.spec.ts
asserts that marker against a local relay, and those assertions cannot run
here (no relay), so a marker change in that file would have shipped unverified.
The PR no longer touches MessageTimeline at all.

Signed-off-by: Max Lampert <maxwell@squareup.com>
Maxwellimus added a commit that referenced this pull request Aug 28, 2026
Converging review found this at P1, two lenses independently, both with
probes against the installed query-core source. Reproduced here.

The prefetch marker records that a channel had a prefetch requested, not
which fetch is in flight. On a cold channel `prefetchQuery` does not start a
fetch at all — TanStack dedupes onto the request already running (query.js
honours `cancelRefetch` only when `state.data !== undefined`). So when the
hover dwell fires after the click, which is the ordinary case for a decisive
click inside the 100ms dwell, the marker labels the mounted timeline's own
fetch a "prefetch", and the post-subscribe refresh cancelled it.

Probe, real modules and a real QueryObserver:
  no hover:           { calls: 1, mountFetchAborted: false }
  hover then click:   { calls: 2, mountFetchAborted: true }

That is two sequential relay round trips with the skeleton held across both,
making a hovered channel slower to open than an unhovered one — on the exact
path this stack exists to speed up.

The refresh now waits for the in-flight fetch instead of cancelling it. The
gap it was closing still closes: the invalidate below re-reads the window
after the subscription exists, it just no longer discards the round trip the
user is watching. #6455's one-fetch contract for a cold channel with no
prefetch is unchanged.

The guard's own comment claimed a cold mount fetch "is still parked on the
hydration gate" and so could not be affected. That is false in steady state —
`channelHeadHydration` is an already-resolved promise after boot.

Adds the regression test; it fails on the cancelling version.

Signed-off-by: Max Lampert <maxwell@squareup.com>
RossHartmann added a commit to Kiingo/buzz that referenced this pull request Aug 30, 2026
* chore(desktop): drop unused ORIGINAL_CONTENT from empty-edit-delete spec (#6517)

`biome check` fails with `lint/correctness/noUnusedVariables` on
`ORIGINAL_CONTENT` in `desktop/tests/e2e/empty-edit-delete.spec.ts`,
which fails `pnpm check` (Desktop Core) for **every PR touching desktop
paths** — e.g. it currently blocks #6460. It presumably landed while
Desktop Core was path-skipped on the introducing PR.

One-line removal; the constant has no remaining references (the
assertions use `RENDERED_ORIGINAL_CONTENT`).

Signed-off-by: Max Lampert <maxwell@squareup.com>

* fix(desktop): restore true zoom by scaling the root rem (#6514)

## Summary

Follow-up to #5644. Cmd +/- had become a text-only zoom: type scaled
while rem-based padding, gaps, widths, avatars, and controls stayed
frozen, which produced cramped layouts (see [#buzz-frontend
thread](buzz://message?channel=a410ffde-c61f-416a-96e0-c296b5f5ecc9&id=1a758115cf07b00c097f6e988553908c045165325a57637519cfa7ed9c9accec)).

Root cause: #5644 introduced a virtual typography rem so the **Font
size** preference could change text without moving layout — a good
decoupling — but it also routed **Cmd +/- zoom** through that same
px-valued token and pinned the real root at 16px. One decision ("freeze
layout") was applied to two dials that shouldn't share it.

This PR gives each dial one owner and lets CSS compose them:

| Control | Changes | How |
|---|---|---|
| **Cmd +/- zoom** | Everything — true zoom | Scales the real `<html>`
font-size again (`useWebviewZoomShortcuts`) |
| **Font size preference** | Text only | Sets `data-font-size`;
`typography.css` maps it to a unitless `--buzz-type-scale`, mirroring
how density already works |

`--buzz-type-rem` becomes `calc(1rem * var(--buzz-type-scale))` —
rem-relative, so it rides on zoom automatically. Resulting text px = `16
× zoom × scale × token-ratio`. The 13 / 14 / 15px conversation contract
is unchanged at default zoom. Density and the type ramp from #5644 are
untouched.

The preference module no longer does px math or knows about zoom; the
zoom hook no longer imports the preference module. Net deletion in
production code.

## Validation

- `pnpm test` — 5,308 desktop unit tests
- `pnpm check:px-text`, `tsc --noEmit`, biome
- Playwright: `top-chrome-zoom-clearance.spec.ts` (native-chrome
clearance stays fixed under root zoom),
`inbox-refactor-screenshots.spec.ts` (zoomed row padding now asserts
`4.4px` instead of the frozen `4px`), and both `profile.spec.ts` zoom
tests (composed zoom × preference, cross-window storage reset)
- Before/after screenshots at 140% zoom in the comment below

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* chore(deps): update swatinem/rust-cache digest to 6323deb (#5441)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache)
([changelog](https://github.com/Swatinem/rust-cache/compare/e18b497796c12c097a38f9edb9d0641fb99eee32..6323deb102c322ba6fcbdcafc7e3dddab59af2b6))
| action | digest | `e18b497` → `6323deb` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update ubuntu:24.04 docker digest to 561618e (#5442)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ubuntu](https://hub.docker.com/_/ubuntu)
([source](https://git.launchpad.net/cloud-images/+oci/ubuntu-base)) |
container | digest | `4fbb8e6` → `561618e` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjkuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @tauri-apps/api to v2.11.1 (#5444)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tauri-apps/api](https://github.com/tauri-apps/tauri) |
[`2.11.0` →
`2.11.1`](https://renovatebot.com/diffs/npm/@tauri-apps%2fapi/2.11.0/2.11.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@tauri-apps%2fapi/2.11.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tauri-apps%2fapi/2.11.0/2.11.1?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>tauri-apps/tauri (@&#8203;tauri-apps/api)</summary>

###
[`v2.11.1`](https://github.com/tauri-apps/tauri/releases/tag/%40tauri-apps/api-v2.11.1):
@&#8203;tauri-apps/api v2.11.1

[Compare
Source](https://github.com/tauri-apps/tauri/compare/@tauri-apps/api-v2.11.0...@tauri-apps/api-v2.11.1)

<details>
<summary><em><h4>PNPM Audit</h4></em></summary>

```
No known vulnerabilities found
```

</details>

#### \[2.11.1]
##### Enhancements

-
[`916782601`](https://www.github.com/tauri-apps/tauri/commit/9167826011cc3d114bf12dfb301968fae479891f)
([#&#8203;15520](https://github.com/tauri-apps/tauri/pull/15520)
by [@&#8203;polw1](https://www.github.com/tauri-apps/tauri/../../polw1))
Document that `Monitor.size`, `Monitor.position` and `Monitor.workArea`
are in physical pixels, with examples showing how to convert them to the
logical pixels expected by window creation options via
`toLogical(monitor.scaleFactor)`.

<details>
<summary><em><h4>PNPM Publish</h4></em></summary>

```
> @tauri-apps/api@2.11.1 npm-publish /home/runner/work/tauri/tauri/packages/api
> pnpm build && cd ./dist && pnpm publish --access public --loglevel silly --no-git-checks

> @tauri-apps/api@2.11.1 build /home/runner/work/tauri/tauri/packages/api
> rollup -c --configPlugin typescript

�[36m
�[1m./src/app.ts, ./src/core.ts, ./src/dpi.ts, ./src/event.ts, ./src/image.ts, ./src/index.ts, ./src/menu.ts, ./src/mocks.ts, ./src/path.ts, ./src/tray.ts, ./src/webview.ts, ./src/webviewWindow.ts, ./src/window.ts�[22m → �[1m./dist, ./dist�[22m...�[39m
�[32mcreated �[1m./dist, ./dist�[22m in �[1m883ms�[22m�[39m
�[36m
�[1msrc/index.ts�[22m → �[1m../../crates/tauri/scripts/bundle.global.js�[22m...�[39m
�[32mcreated �[1m../../crates/tauri/scripts/bundle.global.js�[22m in �[1m1.4s�[22m�[39m
npm verbose cli /opt/hostedtoolcache/node/24.16.0/x64/bin/node /opt/hostedtoolcache/node/24.16.0/x64/bin/npm
npm info using npm@11.13.0
npm info using node@v24.16.0
npm silly config load:file:/opt/hostedtoolcache/node/24.16.0/x64/lib/node_modules/npm/npmrc
npm silly config load:file:/tmp/286e8dee195254a4370e608b672019b0/.npmrc
npm silly config load:file:/home/runner/.npmrc
npm silly config load:file:/home/runner/.config/pnpm/rc
npm verbose title npm publish tauri-apps-api-2.11.1.tgz
npm verbose argv "publish" "--ignore-scripts" "tauri-apps-api-2.11.1.tgz" "--access" "public" "--loglevel" "silly"
npm verbose logfile logs-max:10 dir:/home/runner/.npm/_logs/2026-06-17T13_41_23_851Z-
npm verbose logfile /home/runner/.npm/_logs/2026-06-17T13_41_23_851Z-debug-0.log
npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
npm warn Unknown env config "overrides". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
npm silly logfile done cleaning log files
npm verbose publish [ 'tauri-apps-api-2.11.1.tgz' ]
npm http cache file:/tmp/286e8dee195254a4370e608b672019b0/tauri-apps-api-2.11.1.tgz 0ms (cache hit)
npm notice
npm notice 📦  @tauri-apps/api@2.11.1
npm notice Tarball Contents
npm notice 99.3kB CHANGELOG.md
npm notice 10.2kB LICENSE_APACHE-2.0
npm notice 1.1kB LICENSE_MIT
npm notice 3.5kB README.md
npm notice 5.9kB app.cjs
npm notice 5.4kB app.d.ts
npm notice 5.5kB app.js
npm notice 11.2kB core.cjs
npm notice 6.5kB core.d.ts
npm notice 10.7kB core.js
npm notice 11.0kB dpi.cjs
npm notice 8.8kB dpi.d.ts
npm notice 10.8kB dpi.js
npm notice 5.8kB event.cjs
npm notice 4.9kB event.d.ts
npm notice 5.7kB event.js
npm notice 2.2kB external/tslib/tslib.es6.cjs
npm notice 2.2kB external/tslib/tslib.es6.js
npm notice 3.0kB image.cjs
npm notice 2.4kB image.d.ts
npm notice 2.9kB image.js
npm notice 738B index.cjs
npm notice 1.2kB index.d.ts
npm notice 669B index.js
npm notice 1.1kB menu.cjs
npm notice 451B menu.d.ts
npm notice 717B menu.js
npm notice 3.6kB menu/base.cjs
npm notice 887B menu/base.d.ts
npm notice 3.6kB menu/base.js
npm notice 2.2kB menu/checkMenuItem.cjs
npm notice 1.5kB menu/checkMenuItem.d.ts
npm notice 2.2kB menu/checkMenuItem.js
npm notice 7.4kB menu/iconMenuItem.cjs
npm notice 6.1kB menu/iconMenuItem.d.ts
npm notice 7.4kB menu/iconMenuItem.js
npm notice 5.1kB menu/menu.cjs
npm notice 4.4kB menu/menu.d.ts
npm notice 5.0kB menu/menu.js
npm notice 1.7kB menu/menuItem.cjs
npm notice 1.3kB menu/menuItem.d.ts
npm notice 1.6kB menu/menuItem.js
npm notice 1.1kB menu/predefinedMenuItem.cjs
npm notice 2.6kB menu/predefinedMenuItem.d.ts
npm notice 1.1kB menu/predefinedMenuItem.js
npm notice 7.1kB menu/submenu.cjs
npm notice 4.8kB menu/submenu.d.ts
npm notice 6.9kB menu/submenu.js
npm notice 9.8kB mocks.cjs
npm notice 5.0kB mocks.d.ts
npm notice 9.7kB mocks.js
npm notice 1.8kB package.json
npm notice 22.7kB path.cjs
npm notice 17.7kB path.d.ts
npm notice 21.7kB path.js
npm notice 7.1kB tray.cjs
npm notice 8.5kB tray.d.ts
npm notice 7.0kB tray.js
npm notice 20.7kB webview.cjs
npm notice 23.8kB webview.d.ts
npm notice 20.5kB webview.js
npm notice 8.4kB webviewWindow.cjs
npm notice 4.9kB webviewWindow.d.ts
npm notice 8.3kB webviewWindow.js
npm notice 68.1kB window.cjs
npm notice 64.9kB window.d.ts
npm notice 67.2kB window.js
npm notice Tarball Details
npm notice name: @tauri-apps/api
npm notice version: 2.11.1
npm notice filename: tauri-apps-api-2.11.1.tgz
npm notice package size: 135.7 kB
npm notice unpacked size: 699.0 kB
npm notice shasum: cd6b13fc26403ca095a02e39ecdbec8048d2872d
npm notice integrity: sha512-M2FPuYND2m+wh[...]sUepJWugQCvAA==
npm notice total files: 67
npm notice
npm http fetch GET https://run-actions-1-azure-eastus.actions.githubusercontent.com/113//idtoken/***/***?api-version=2.0&audience=npm%3Aregistry.npmjs.org 200 76ms
npm http fetch POST 201 https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/@tauri-apps%2fapi 674ms
npm verbose oidc Successfully retrieved and set token
npm http fetch GET 200 https://registry.npmjs.org/@tauri-apps%2fapi 54ms (cache miss)
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice publish Signed provenance statement with source and build information from GitHub Actions
npm notice publish Provenance statement published to transparency log: https://search.sigstore.dev/?logIndex=1851797040
npm http fetch PUT 200 https://registry.npmjs.org/@tauri-apps%2fapi 2070ms
+ @tauri-apps/api@2.11.1
npm verbose cwd /tmp/286e8dee195254a4370e608b672019b0
npm verbose os Linux 6.17.0-1018-azure
npm verbose node v24.16.0
npm verbose npm  v11.13.0
npm verbose exit 0
npm info ok
```

</details>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update rust crate futures to v0.3.33 (#5445)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [futures](https://rust-lang.github.io/futures-rs)
([source](https://github.com/rust-lang/futures-rs)) |
dev-dependencies | patch | `0.3.32` → `0.3.34` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>rust-lang/futures-rs (futures)</summary>

###
[`v0.3.34`](https://github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11)

[Compare
Source](https://github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34)

- Preserve cloned waker identity.
([#&#8203;3032](https://github.com/rust-lang/futures-rs/issues/3032))
- Updato `syn` to 3.
([#&#8203;3028](https://github.com/rust-lang/futures-rs/issues/3028))

###
[`v0.3.33`](https://github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18)

[Compare
Source](https://github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33)

- Fix `ReadLine`'s soundness issue regarding to exception safety.
([#&#8203;3020](https://github.com/rust-lang/futures-rs/issues/3020))
- Fix unsound `Send` impl for `IterPinRef` and `Iter`.
([#&#8203;3003](https://github.com/rust-lang/futures-rs/issues/3003))
- Fix stacked borrows violation in `compat01as03` implementation.
([#&#8203;3012](https://github.com/rust-lang/futures-rs/issues/3012))
- Fix memory leak in `FuturesUnordered::IntoIter`.
([#&#8203;3005](https://github.com/rust-lang/futures-rs/issues/3005))
- Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`.
([#&#8203;3007](https://github.com/rust-lang/futures-rs/issues/3007))
- Re-export `alloc::task::Wake`.
([#&#8203;3010](https://github.com/rust-lang/futures-rs/issues/3010))
- Update `spin` to 0.12.
([#&#8203;3014](https://github.com/rust-lang/futures-rs/issues/3014))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjkuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update rust crate futures-util to v0.3.33 (#5448)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [futures-util](https://rust-lang.github.io/futures-rs)
([source](https://github.com/rust-lang/futures-rs)) |
dependencies | patch | `0.3.32` → `0.3.34` |
| [futures-util](https://rust-lang.github.io/futures-rs)
([source](https://github.com/rust-lang/futures-rs)) |
workspace.dependencies | patch | `0.3.32` → `0.3.34` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>rust-lang/futures-rs (futures-util)</summary>

###
[`v0.3.34`](https://github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11)

[Compare
Source](https://github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34)

- Preserve cloned waker identity.
([#&#8203;3032](https://github.com/rust-lang/futures-rs/issues/3032))
- Updato `syn` to 3.
([#&#8203;3028](https://github.com/rust-lang/futures-rs/issues/3028))

###
[`v0.3.33`](https://github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18)

[Compare
Source](https://github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33)

- Fix `ReadLine`'s soundness issue regarding to exception safety.
([#&#8203;3020](https://github.com/rust-lang/futures-rs/issues/3020))
- Fix unsound `Send` impl for `IterPinRef` and `Iter`.
([#&#8203;3003](https://github.com/rust-lang/futures-rs/issues/3003))
- Fix stacked borrows violation in `compat01as03` implementation.
([#&#8203;3012](https://github.com/rust-lang/futures-rs/issues/3012))
- Fix memory leak in `FuturesUnordered::IntoIter`.
([#&#8203;3005](https://github.com/rust-lang/futures-rs/issues/3005))
- Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`.
([#&#8203;3007](https://github.com/rust-lang/futures-rs/issues/3007))
- Re-export `alloc::task::Wake`.
([#&#8203;3010](https://github.com/rust-lang/futures-rs/issues/3010))
- Update `spin` to 0.12.
([#&#8203;3014](https://github.com/rust-lang/futures-rs/issues/3014))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update rust crate http to v1.4.2 (#5451)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [http](https://github.com/hyperium/http) | dependencies |
patch | `1.4.0` → `1.4.2` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>hyperium/http (http)</summary>

###
[`v1.4.2`](https://github.com/hyperium/http/blob/HEAD/CHANGELOG.md#142-June-8-2026)

[Compare
Source](https://github.com/hyperium/http/compare/v1.4.1...v1.4.2)

- Fix `uri::Builder` to allow `"*"` as the path when scheme and
authority are also set, used in HTTP/2 requests.
- Fix `Uri` to properly reject `DEL` characters.

###
[`v1.4.1`](https://github.com/hyperium/http/blob/HEAD/CHANGELOG.md#141-May-25-2026)

[Compare
Source](https://github.com/hyperium/http/compare/v1.4.0...v1.4.1)

- Fix `PathAndQuery::from_static()` and `from_shared()` to reject inputs
that do not start with `/`.
- Fix `Extend` for `HeaderMap` to clamp max size hint and not overflow.
- Fix `header::IntoIter` that could use-after-free if the generic value
type could panic on drop.
- Fix `header::{IterMut, ValuesIterMut}` to not violate stacked borrows.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update rust crate http-body-util to v0.1.4 (#5452)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [http-body-util](https://github.com/hyperium/http-body) |
dependencies | patch | `0.1.3` → `0.1.5` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>hyperium/http-body (http-body-util)</summary>

###
[`v0.1.5`](https://github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5)

[Compare
Source](https://github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5)

###
[`v0.1.4`](https://github.com/hyperium/http-body/releases/tag/http-body-util-v0.1.4)

[Compare
Source](https://github.com/hyperium/http-body/compare/http-body-util-v0.1.3...http-body-util-v0.1.4)

#### What's Changed

- Add `Fused` body combinator that always returns `None` once completed.
- Add `BodyExt::into_stream()` to convert a body into a `Stream`.
- Add `Full::into_inner()` to get the full `Buf`.
- Add `InspectFrame` and `InspectErr` combinators.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency sonner to v2.0.8 (#6093)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [sonner](https://sonner.emilkowal.ski/)
([source](https://github.com/emilkowalski/sonner)) | [`2.0.7` →
`2.0.8`](https://renovatebot.com/diffs/npm/sonner/2.0.7/2.0.8) |
![age](https://developer.mend.io/api/mc/badges/age/npm/sonner/2.0.8?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/sonner/2.0.7/2.0.8?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>emilkowalski/sonner (sonner)</summary>

###
[`v2.0.8`](https://github.com/emilkowalski/sonner/compare/v2.0.7...ecce1841c55e4a72dfe139a8992b56498660125e)

[Compare
Source](https://github.com/emilkowalski/sonner/compare/v2.0.7...v2.0.8)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOS41IiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update rust crate async-trait to v0.1.92 (#6094)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-trait](https://github.com/dtolnay/async-trait) |
dependencies | patch | `0.1.91` → `0.1.92` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>dtolnay/async-trait (async-trait)</summary>

###
[`v0.1.92`](https://github.com/dtolnay/async-trait/releases/tag/0.1.92)

[Compare
Source](https://github.com/dtolnay/async-trait/compare/0.1.91...0.1.92)

- Resolve double\_must\_use clippy lint in generated code
([#&#8203;303](https://github.com/dtolnay/async-trait/issues/303))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOS41IiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(composer): preserve caret when inserting mentions mid-message (#6531)

**Category:** fix
**User Impact:** Users can insert mentions earlier in a draft and
continue typing without the caret corrupting the rest of the message.

**Problem:** Caret correction ran after every document change, so typing
a mention before existing text repeatedly advanced across the mention
separator and interleaved spaces into the draft. **Solution:** Limit
correction to the autocomplete settlement it was designed for, with
transaction-level and browser-level regression coverage for known and
unregistered mentions.

<details>
<summary>File changes</summary>

**desktop/src/features/messages/lib/mentionHighlightExtension.ts**
Restricts trailing-space caret advancement to an armed autocomplete
settlement instead of every document change.

**desktop/src/features/messages/lib/mentionHighlightExtension.test.mjs**
Exercises the real ProseMirror plugin state and verifies mid-draft
mention typing, unknown tokens, end-of-message typing, and
completed-mention separators.

**desktop/tests/e2e/mentions.spec.ts**
Reproduces the reported composer workflow in Chromium and covers the
same corruption path for an unregistered `@token`.

</details>

## Reproduction steps

1. Open a channel and enter `hello world` in the composer.
2. Move the caret between `hello` and ` world`.
3. Type ` @bo`, select `bob` from autocomplete, and continue typing
`abc`.
4. Confirm the composer reads `hello @bob abc world` with the caret
after `abc`.
5. Repeat with an unregistered token such as ` @zzq` and confirm the
existing text remains intact.

## Before / After

| Before | After |
| --- | --- |
| Typing after a mid-draft mention walks the caret through the existing
message. | Continued typing stays after the inserted mention. |
| ![Before: mention caret corrupts existing draft
text](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6531/mention-caret-before.gif)
| ![After: caret remains after the inserted
mention](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6531/mention-caret-after.gif)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>

* fix(desktop): remove Buzz entity link previews (#6512)

**Category:** improvement
**User Impact:** Buzz-native project, repository, issue, and pull
request links now appear once as compact inline chips, with their
details available on hover.

**Problem:** Buzz-native entity links rendered both an inline chip and a
standalone preview card, repeating the same metadata and adding visual
noise to conversations. **Solution:** Exclude Buzz-native links from the
shared standalone-preview extractor while leaving entity parsing intact
for chip tooltips and preserving external web previews and attachment
cards.

<details>
<summary>File changes</summary>

**desktop/src/shared/lib/linkPreview.ts**
Stops Buzz-native preview candidates after parsing, including same-relay
git clone URLs that normalize to repository entities, while allowing
external URLs through the existing snapshot path.

**desktop/src/shared/lib/linkPreview.test.mjs**
Covers project, repository, issue, pull request, markdown-labeled,
same-relay clone, and mixed external-link extraction behavior.

**desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs**
Confirms sent messages no longer merge a standalone Buzz entity card
while external sender snapshots still render.

</details>

## Reproduction steps

1. Open a desktop channel containing a `buzz://project`, `buzz://repo`,
`buzz://issue`, or `buzz://pr` link.
2. Confirm the link renders as an inline entity chip without a second
standalone Buzz card below the message.
3. Hover the chip and confirm its entity metadata remains available.
4. Post an external HTTPS link and confirm its web preview still
renders.
5. Paste a same-relay `/git/<owner>/<repo>` clone URL and confirm it
uses the repository chip without a duplicate card.

## Screenshots

| Before | After |
| --- | --- |
| Inline chip plus redundant standalone Project card | Inline chip is
now the sole presentation |
| ![Before: project chip and duplicate standalone
card](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--before.png)
| ![After: project chip without a standalone
card](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--after.png)
|

**After — rich metadata stays available on hover**

![After: dark theme with pink accent and Project tooltip showing
description and repository
count](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--after-tooltip-rich.png)

## Verification

At commit `3fa74cdd342ac1f6721b7d56a7f111af31e0e6e9`:

- focused link-preview + Markdown unit suites — 119/119 passed
- targeted registered smoke E2E — 8/8 passed, including labeled
same-relay clone metadata, ordinary-link presentation, and in-app
navigation
- `cd desktop && pnpm exec tsc --noEmit` — passed
- `git diff --check origin/main...HEAD` — passed
- pre-push hooks — desktop check, TypeScript, and full desktop unit
suite passed

---------

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>

* feat(desktop-messages): keep agents addressed across messages (#6315)

**Category:** new-feature
**User Impact:** Users can keep selected agents addressed across
consecutive messages without retyping their handles.

**Problem:** Repeated conversations with agents require manually typing
the same mentions on every turn, which adds friction and makes
recipients easy to omit.

**Solution:** The composer can now keep agents automatically addressed
per channel, either from the mention controls or after a successful
inline mention. Addressed agents remain visible in the toolbar, apply to
channel threads, survive send failures safely, and never cross community
boundaries.

## Changes

<details>
<summary>File changes</summary>

**desktop/src-tauri/src/events/message_tags.rs**  
Preserves the automatic-address marker on validated mention reference
tags.

**desktop/src/features/channels/ui/ChannelPane.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/communities/useCommunityInit.ts**  
Clears composer audience state when the active community changes.

**desktop/src/features/forum/ui/ForumComposer.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/forum/ui/ForumComposerAutocompletes.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/home/ui/InboxDetailPane.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/messages/lib/agentAddressMention.d.mts**  
Defines helpers and types for marked automatic-address mention tags.

**desktop/src/features/messages/lib/agentAddressMention.mjs**  
Defines helpers and types for marked automatic-address mention tags.

**desktop/src/features/messages/lib/agentAddressMention.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/lib/applyEditTagOverlay.mjs**  
Preserves automatic-address metadata when edited message tags are
overlaid.

**desktop/src/features/messages/lib/applyEditTagOverlay.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Stores the preference that keeps explicitly mentioned agents addressed
for later messages.

**desktop/src/features/messages/lib/extractMentionPersonas.ts**  
Separates persona recipients from the composer mention orchestration.

**desktop/src/features/messages/lib/persistentAgentAudience.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/lib/persistentAgentAudience.ts**  
Maintains bounded, in-memory, channel-scoped automatic agent audiences.

**desktop/src/features/messages/lib/useMentionSelection.ts**  
Centralizes mention picker selection state and agent-first selection
behavior.

**desktop/src/features/messages/lib/useMentions.ts**  
Exposes explicit picker origins and selection controls while preserving
inline mention behavior.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**  
Renders compact addressed-agent avatars and the automatic-mention
management entry point.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**  
Adds automatic-mention controls and options to the existing mention
picker.

**desktop/src/features/messages/ui/MessageAgentAddressPrefix.tsx**  
Shows which agents were automatically addressed on a sent message.

**desktop/src/features/messages/ui/MessageComposer.tsx**  
Integrates automatic audiences, picker controls, accessible feedback,
shortcuts, and send behavior.

**desktop/src/features/messages/ui/MessageComposer.types.ts**  
Defines the simplified channel audience context shared by composer
hosts.

**desktop/src/features/messages/ui/MessageComposerToolbar.tsx**  
Places automatic-address controls in the composer toolbar without
crowding narrow layouts.

**desktop/src/features/messages/ui/MessageRow.tsx**  
Displays automatic-address metadata alongside sent message content.

**desktop/src/features/messages/ui/MessageThreadPanel.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAddressMentionPulse.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAddressMentionPulse.ts**  
Provides success and failure animation signals for addressed-agent
controls.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**  
Coordinates adding, removing, and announcing automatically addressed
agents.

**desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts**  
Implements the platform-aware shortcut for toggling automatic
addressing.

**desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts**  
Promotes successfully sent inline agent mentions and provides a single
undoable notification.

**desktop/src/features/messages/ui/useComposerMentionPicker.ts**  
Opens the mention picker without rewriting the current draft.


**desktop/src/features/messages/ui/useMentionSendFlow.helpers.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts**  
Merges automatic and inline recipients, marks outgoing tags, and
restores failed sends safely.

**desktop/src/features/messages/ui/useMentionSendFlow.ts**  
Merges automatic and inline recipients, marks outgoing tags, and
restores failed sends safely.


**desktop/src/features/messages/ui/usePersistentAgentMentionHydration.ts**
Removes the prior draft-text hydration approach now that automatic
audiences stay at composer ingress.

**desktop/src/features/settings/ui/AgentsSettingsPanel.tsx**  
Replaces the old global behavior with explicit composer-level
automatic-mention controls.

**desktop/src/features/settings/ui/PreventSleepSettingsCard.tsx**  
Replaces the old global behavior with explicit composer-level
automatic-mention controls.

**desktop/src/shared/lib/keyboard-shortcuts.ts**  
Defines the user-facing automatic-address keyboard shortcut label.

**desktop/src/shared/ui/VideoReviewCommentMarkdown.tsx**  
Allows automatic-address prefixes to compose with video review
timecodes.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

</details>

## Reproduction Steps

1. Open a channel with one or more agents and open the mention picker
from the composer.
2. Select an agent for automatic mentions, then send several messages
without retyping the handle; confirm the agent remains in the composer
control and receives each message.
3. Mention another agent inline, send successfully, and confirm the
agent becomes automatically addressed; use the notification's Undo
action to reverse it.
4. Open a thread in the same channel and confirm the same addressed
agents are available there.
5. Remove an agent from the composer control and confirm later messages
stop addressing it.
6. Switch communities and confirm addressed agents do not carry into the
other community.

## Screenshots

All states below use the dark Buzz theme with a selected lilac accent.

### Addressed composer

Selected agents stay visible at the composer ingress without adding
handles to the draft.

![Two automatically addressed agents in the dark composer with a lilac
accent](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--01-addressed-composer.png)

### Open mention menu

The @ ingress opens the existing mention menu and shows which agents are
already addressed.

![Open mention menu with automatically addressed agents
highlighted](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--02-open-mention-menu.png)

### Mention options

The inline options pane controls whether a successful one-time agent
mention carries into later messages.

![Automatic mention options expanded above the mention
menu](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--03-mention-options.png)

### Agent settings

The same preference is available in **Settings → Agents →
Conversations**.

![Automatic agent mentions preference in the Agents settings
pane](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--04-agent-settings.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>

* Add mobile Huddles voice MVP (#6056)

## Summary
- add foreground mobile Huddles on Android and iOS with native Opus
capture/playback, mute, speaker routing, participants, lifecycle, and
minimized drawer UI
- keep mobile Huddle cards and roster state live, including ended rooms,
relay-resolved profiles, and agents
- broadcast desktop agent TTS through the existing Huddle audio protocol

## Scope
Foreground human-to-human voice MVP only. Agent setup/transcripts,
background calling, recording, and advanced device controls remain out
of scope.

## Validation
- `just mobile-check`
- `just mobile-test` — 1,500 passed
- `just desktop-check` and `just desktop-test` — 4,957 passed
- desktop typecheck, strict Clippy, and Tauri tests — 2,445 passed, 15
ignored
- mobile worktree identity contract checks
- physical Pixel/iPhone behavior reviewed during development

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>

* refactor(acp): clarify agent prompt sections (#6501)

## Why
The ACP prompt puts a machine-specific Workspace prefix before static
base guidance and labels the user-facing agent instruction layer as the
generic System section. Because the cwd varies by launch and worktree,
leading with it reduces reusable prompt-prefix stability.

`[Workspace]` was added in [PR
#1194](https://github.com/block/buzz/pull/1194) as a defensive fix after
a broken `~/.sprout` → `~/.buzz` migration caused agents to scan `$HOME`
and trigger macOS TCC prompts. This change retains that grounding while
shrinking it to the current working directory and moving dynamic
environment context after the static Base prompt.

## What
- Emit the prompt in Base → Workspace → Agent Instructions order
- Reduce Workspace to `Current working directory: <absolute path>`
- Resolve cwd as an absolute native-platform path and preserve Windows
drive/UNC paths instead of checking for a leading `/`
- Emit Agent Instructions for persona and standalone agent instructions
across modern and legacy ACP paths
- Preserve parsing for archived observer frames that used System or the
former Workspace-before-Base order, and align the persona catalog label

## Risk Assessment
Medium-low — this changes prompt framing for every newly created agent
session. Existing archived observer frames remain parseable, and
execution still uses the same ACP working directory. Cwd resolution now
fails clearly instead of substituting `/` when the process directory
cannot be resolved.

## References
- https://github.com/block/buzz/pull/1103
- https://github.com/block/buzz/pull/1194

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>

* perf(desktop): make the Projects surface render-cheap (#6460)

Diagnostic profiling on a large community (101 issues, 258 PRs) showed
Projects tab switches taking 2.5–3.6s, dominated by single React commits
of 0.5–1.5s and per-render recomputation — fetch work was already off
the main thread; the cost was building the UI.

### Measured: tab click → painted, per tab

| tab | before | after |
|---|---|---:|
| projects | 3,608ms | 320–580ms |
| repositories | 3,126–3,534ms | 310–410ms |
| tasks | 395–1,101ms | ~115ms |
| reviews | 322–2,603ms | ~96ms |
| activity | 597–741ms | ~148ms |

Single-commit ceiling dropped from 1,541ms to ≤200ms (growth steps
25–40ms). Fixes in profiled-cost order:

- **Profile popover body mounts only while open.** `UserProfilePopover`
carried seven query subscriptions plus interaction hooks per instance
even when closed; grids mount hundreds (five per card in people stacks,
one per row author) — measured **~40ms per card**, the dominant share of
the 1.2s card-tab commits. The always-mounted shell is now just the
Radix root + trigger; trigger markup, hover timing, and keyboard
handling are unchanged, and hover/tooltip event continuity is preserved
because the trigger never remounts.
- **Incremental row mounting.** The first 12 cards / 30 rows render in
the first commit; the rest stream in 36–60-per-frame low-priority
transitions. Grouped lists trim across group boundaries via a pure,
tested slicer; the mounted count survives in-place refetches.
- **Activity feed**: was rebuilt unmemoized on every render,
markdown-flattening every issue/PR/comment body in the community just to
sort and keep 30 items (~360+ flattens per render on the measured
community). Now memoized, and bodies stay raw until after the sort+slice
— 30 flattens, once per data change.
- **Contribution graph** (always-visible rail, so every tab paid for
it): ~180 day cells each wrapped in a Radix tooltip with per-cell Intl
date formatting per render. Now memoized, cells precomputed once per
data change, native `title` tooltips. (The activity-bar segments keep
their styled Radix tooltips — pinned by an existing spec.)
- **Rows/cards memoized with identity-stable props**: per-row selection
arrays were rebuilt per row per render (O(n²) — 258 PRs × 258-item
arrays each render) and are now hoisted and shared; people arrays derive
inside the memoized cards; the rail's stat walk over every issue/PR is
memoized.
- **`content-visibility: auto`** on cards and rows so offscreen entries
skip layout and paint; **tab switches run in a React transition** so the
click stays responsive while the new tree mounts.

Remaining known cost (out of scope): cold-entry data readiness — the
work-item and activity queries ship thousands of events to compute
counts (2–4s on a large community; see the fan-lifecycle PR). The
structural fix is a relay-side aggregate; tracked as follow-up.

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>

* Downgrade mobile Huddles to audio protocol v2 (#6558)

## Summary

- downgrade Mobile Huddle authentication and native media configuration
from protocol v3 to the currently deployed relay's v2 contract
- restore the released one-byte relay peer prefix while retaining later
reconnect, roster, and playout-reset reliability fixes
- update Android, iOS, protocol documentation, and focused tests
together

Protocol v2 does not carry v3's occupancy epoch on audio frames, so it
cannot fence the narrow delayed-packet/peer-index-reuse race. This is an
intentional compatibility tradeoff until the relay v3 rollout is ready.

### Related issue

None found.

### Testing

- `just mobile-check`
- `just mobile-test` — 1,661 tests passed
- Android debug build installed and launched on Pixel 10 as
`xyz.block.buzz.mobile.sprout_mobile_profile_settings`; foreground
process verified
- signed iOS Release build installed and launched on iPhone as
`com.buzz.buzzMobile`; running process verified

A live two-device Huddle audio call remains a manual verification step.

Signed-off-by: kenny lopez <klopez4212@gmail.com>

* Polish Huddle participant interactions (#6312)

## Summary
- arrange Huddle participants in a responsive, equal-weight cluster with
spring enter/exit motion and a `+N` overflow
- spotlight tapped participants over a blurred call surface, with a
roster for hidden participants and no self-avatar action
- add selection haptics across full-screen and drawer controls,
including both end-call buttons
<img width="1080" height="2424" alt="Screenshot_20260819-151448"
src="https://github.com/user-attachments/assets/00b7fdca-2304-4788-9952-e07224798513"
/>
<img width="1080" height="2424" alt="Screenshot_20260819-151422"
src="https://github.com/user-attachments/assets/a0cfc861-0519-44ff-bb56-4c983ed6344c"
/>

## Validation
- `just mobile-check`
- focused participant, drawer-control, and full-screen end-call widget
tests
- Huddle-focused widget suite (15 tests)
- full mobile Flutter suite (1,538 tests)

## Dependency
Built on #6056 and contains only the follow-up interaction work. Merge
after #6056 lands.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>

* Downgrade desktop Huddles to audio protocol v2 (#6610)

## Summary
- negotiate Huddle audio protocol v2 on desktop
- decode the released one-byte peer-index prefix
- retain roster-driven playout resets and document the missing v3 epoch
fence

## Testing
- `just desktop-tauri-fmt-check`
- `just desktop-tauri-clippy`
- `just desktop-tauri-test`

Signed-off-by: kenny lopez <klopez4212@gmail.com>

* perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)

## Summary

Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.

**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).

Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.

**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.

**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.

**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).

### Related issue
Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.

### Review follow-ups
Addressing Carl's reviews
[5001114109](https://github.com/block/buzz/pull/6572#pullrequestreview-5001114109)
and
[5002596542](https://github.com/block/buzz/pull/6572#pullrequestreview-5002596542),
each pushed as new commits (no rebase):

- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.

### Testing
At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).

Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70bb + a
comments-only commit correcting two `profile/hooks.ts` freshness
comments from 60s to 10 min; pre-push desktop check/typecheck/test
5,387/0 re-ran at 0c492366d) in one shell; `origin/main` = `040b203f7`
at PR open, since moved to `4baccd539` (#6558, mobile only — zero file
overlap, `git merge-tree` clean):

- `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level
call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount
then 1 on invalidate with wholesale replacement)
- Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` +
`channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at
`7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec
persists a head, reloads into a fresh mock relay with the head fetch
held 5s, asserts the persisted row paints within 2s, exactly one
`get_channel_window` after open, and the stale row is removed when the
authoritative page lands.
- `pnpm typecheck`, `pnpm check` — clean

At `7acbf951b` (everything except the two-line `useThreadReplies.ts`
staleTime revert and the test-only `relay-reconnect.spec.ts` change),
also green in one shell:
- `just desktop-tauri-test` — 2,859 passed / 0 failed across the
workspace (channel_head_cache: wire shape, LRU+caps, schema reset,
corrupt-row skip)
- `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p
buzz-relay --lib` — 908 passed / 0 failed
- `just check` components: fmt-check, clippy, desktop-check,
desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy,
web-check, mobile-check, file-size-check — all green
- `just desktop-build`, `web-build`, `desktop-tauri-check`,
`mobile-test` (1,661 passed) — all green

CI note: the "Unit Tests" job goes red on this PR and on `main` whenever
it hits a poisoned `rust-cache` entry (an empty-directory skeleton of
`target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts),
surfacing as `could not find native static library sherpa-onnx-c-api` in
`buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry
and rerunning turned the job green at `0c492366d` (28/28); it re-poisons
on the next `main` push until the workflow clears that directory after
cache restore.

Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and
line-by-line by me before opening; the staleTime fix re-verified by Wren
and me independently; the relay-reconnect test fix bisected and verified
by me.

---------

Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>

* fix(desktop): keep member runtime status off the UI thread (#6445)

## Summary
- skip managed-agent runtime discovery when the members sidebar has no
local managed bots
- run runtime listing disk, process, and mutex work on Tauri’s blo…
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