Skip to content

perf(desktop): channel-switch tracing + high-membership perf harness - #6455

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

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

Conversation

@Maxwellimus

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 layer used to diagnose and verify the fixes in the follow-up PRs; it changes no product behavior.

Tracing (channelSwitchPerf.ts): every channel navigation records click → route commit → settled paint, with the two relay fetches that can sit on that path (message window, member roster) attributed only when they complete inside the switch window:

[switch-perf] channel=dc3f12db total=739ms commit=+397ms window=89 events in 307ms members=51 members in 273ms
[switch-perf] channel=29414326 total=24ms  commit=+19ms window=cache members=cache

Each switch also emits User Timing marks (buzz:channel-switch:*) and appends a JSONL record via a new Tauri command to {app_log_dir}/switch-perf.jsonl, stamped with the build's git revision (baked by build.rs) and an optional BUZZ_PERF_LOG_LABEL run label — so before/after sessions are attributable offline. Settle waits (bounded) for the timeline's deferred commit so render-heavy switches aren't underreported; forum surfaces abandon the trace rather than underreport; a settle for a different channel never clobbers a newer switch's trace; the active trace resets on community switch.

Harness: an inflateChannelMembers mock-bridge knob (channel name → member count, synthetic members appended on first read) and member-heavy-switch.perf.ts, measuring warm-switch wall time + longtasks for channel↔channel and channel↔Projects at baseline / 2k / 10k members with fixed message volume.

Baselines this instrument established (pre-fix)

measurement value
mock harness, warm channel↔channel wall (median, 4× throttle) 394ms baseline → 574ms at 10k members
mock harness, longtask total per switch at 10k members 364ms
live community, click→settled paint (median, n=26) 310ms; p90 994ms
live community, roster fetch frequency nearly every switch (30s staleness)

The follow-up PRs cut the live median to 201ms (−35%) and warm switches to 148ms (−43%), verified with this same instrument.

@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 and others added 3 commits August 24, 2026 13:25
Add a single-active-trace instrument for channel navigation: goChannel
opens the trace, ChannelScreen marks route commit and settles it after
the timeline loading latch clears (double rAF so the frame painted).
The two relay fetches that can sit on the switch path — the message
window and the member roster — are attributed to the trace when they
run during it; cache-served switches log "cache".

Each switch emits one [switch-perf] console line plus User Timing
marks/measures (buzz:channel-switch:*) so before/after comparisons work
identically in a dev build, the Performance panel, and Playwright perf
specs.

Signed-off-by: Max Lampert <maxwell@squareup.com>
Add an inflateChannelMembers mock-bridge knob (channel name → target
member count; synthetic hex-pubkey members appended once, on first
channel read) and a member-heavy-switch perf spec that measures warm
switch wall time and longtasks for channel↔channel and
channel↔Projects at baseline / 2k / 10k members per channel, holding
message volume constant at 150 rows. Method mirrors
warm-switch-markdown.perf.ts: in-page click + rAF polling, 4x CPU
throttle, medians over 16 switches after an untimed warmup round-trip.

Run from desktop/:
  pnpm build:e2e
  npx playwright test --config=playwright.perf.config.ts member-heavy-switch.perf.ts

Signed-off-by: Max Lampert <maxwell@squareup.com>
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.

@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 e50edba1021f202df5e5feb052afd468b3056918 against base f6e6617a9dcc2308d5039f8afaab974b49fb9577 for three remaining instrumentation-correctness issues:

  1. [P2] Do not leave a trace when channel navigation is refused. goChannel calls beginChannelSwitchTrace(channelId) before commitNavigation, but commitNavigation may return early when allowNavigation rejects the switch. That leaves the target trace active even though navigation never occurred. Repro: visit B, return to A, begin editing a thread reply, click B and reject navigation, then resolve the edit and use browser Back to B within 30 seconds. History navigation is deliberately untraced, yet B's mount settles the orphan and emits an inflated measurement spanning the refused click and edit. Start the trace only after the guard accepts, before navigate, and cover refused click followed by history navigation.

  2. [P2] Include the JSONL terminator in the final 4 KiB record cap. shape_perf_log_line accepts a serialized line whose length is exactly 4096 bytes, then append_line_rotating writes it with writeln!, producing a 4097-byte on-disk record. Reserve one byte for \n and add an exact-boundary test that measures the bytes written, not only the pre-write string length.

  3. [P2] Make the channel↔Projects benchmark measure and report the claimed workload. The harness declares Projects ready when projects-page-header appears, while repository snapshots and work items can still be loading. Its warmup also populates both surfaces, and runScenario combines channel→Projects and Projects→channel samples into one median, which can represent neither direction and hide a one-leg regression. Gate readiness on the query-dependent Projects surface or explicitly relabel this as shell-only warm navigation, and report each direction separately.

Validation: focused JS lifecycle/fetch tests passed 29/29; git diff --check and applicable GitHub checks are green. The Rust boundary test could not run locally because the required Tauri sidecar binary is absent; exact-head Rust and Windows CI passed.

…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>
@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

All three findings from the latest review addressed in 41b8159, then the full cumulative diff was re-reviewed by two independent reviewers with no authoring context (details at the end).

P2 — trace left behind by a guard-refused navigation: the trace now opens inside the shared commit flow (commitGuardedNavigation.ts), strictly after allowNavigation accepts and before navigate. The flow is extracted and dependency-injected; new unit tests pin the ordering (guard → begin → navigate), the same-destination no-op, and the requested regression: a guard-refused click followed by a history-navigation settle records nothing.

P2 — 4 KiB cap vs the newline terminator: shape_perf_log_line now reserves one byte for the \n that writeln! appends (line.len() + 1 > MAX_RECORD_BYTES). The new boundary test accepts a line at exactly 4095 bytes, appends it, asserts the file is exactly 4096 bytes on disk, and rejects one byte more.

P2 — channel↔Projects benchmark honesty: the Projects surface now exposes data-projects-hydrating on its layout root, driven by the overview's query fan (projects, work items, repo snapshots, project + repository activity summaries, local repositories — via isLoading so disabled queries can never wedge it), and the harness treats a present marker as not-ready instead of stopping at the shell header. runScenario now reports each direction as its own median — the first baseline run already justified the split (general→deep-history 363.7ms vs deep-history→general 162.7ms, previously blended). Also hardened while there: member inflation is verified on both inflated channels (the bridge silently skips unknown channel names), and general's ready-selector is prefixed (mock-general-) so the previous channel's rows can't satisfy it.

Fresh-eyes review round: after the fixes, a blind adversarial reviewer (no authoring context, prompted to refute) and codex review both re-reviewed the full base→tip diff. Codex: zero findings. The blind reviewer confirmed all three prior findings genuinely fixed and raised one new P2, also fixed in this push: a settle wait that hits its 5s deadline while the render is still pending now records with settleWaitTruncated: true (measure detail, console line, JSONL) instead of posing as an ordinary settle — keeping the >5s tail visible rather than silently truncating it. Its dead-code nit (ChannelSwitchFetchTrace) is removed.

Deferred (P3, tracked here deliberately): (a) the settle wait polls the global [data-render-pending="true"] selector, so a thread panel opened mid-switch can extend the wait (bounded by the 5s deadline, now flagged as truncated when hit) — scoping it to the channel content container is follow-up work; (b) a double-click on the same sidebar item within one render can restart the trace or open one that only times out — both err toward dropped/shortened measurements, never inflated; (c) the Projects marker omits the profile-batch query (declared below the marker; affects only the untimed warmup leg); (d) build.rs reruns when .git/logs/HEAD is absent, and gitSha holds a git describe string when tags exist — attribution-only cosmetics.

Validation: desktop unit suite 5,456/5,456 (incl. 4 new commitGuardedNavigation tests and 2 new truncation tests); Rust perf_log 10/10 with fmt + clippy clean; settle + navigation smoke specs 20/20; member-heavy perf baseline green with per-direction medians; file-size, px-text, and whitespace gates clean.

Maxwellimus and others added 7 commits August 24, 2026 20:27
…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>
klopez4212 pushed a commit that referenced this pull request Aug 25, 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>
…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>
klopez4212 pushed a commit that referenced this pull request Aug 25, 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>
Maxwellimus and others added 7 commits August 25, 2026 00:41
… 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>
@Maxwellimus

Copy link
Copy Markdown
Contributor Author

🤖 Comment from Maxwell's AI agent.

Since the last review response, this branch went through a multi-round adversarial review loop: 23 completed fresh-eyes reviews rotating across three models and three reasoning-effort levels, each blind to authorship, each prompted to refute, looping until three consecutive reviews found nothing. The last three rounds returned zero findings. Every fix below is its own commit with a red-first test where practical; head is 843e667, CI green.

Measurement-honesty fixes (tracer): frame-starvation guards on every phase of the settle wait (not-pending arrivals past the deadline, per-frame gap ceiling seeded at settle entry, 35s age bound); visibility accounting spanning the whole trace via one module-level visibilitychange watcher (hidden at any point between click and record drops the trace); traces dropped at the navigation layer on any committed non-channel navigation or history traversal — covering traces whose channel screen never mounted; begin() revokes pending same-channel route-exit abandons; the trace (and its start mark, via startTime) anchors at the dispatching input event's timeStamp, so input delay behind a long task is inside totalMs; User Timing buffer bounded to the latest switch across all trace outcomes.

Attribution fixes: the members roster gate is a per-channel supersession token rather than the query AbortSignal — an earlier abort-gate attempt was itself caught by a later round as a regression (reading context.signal flips React Query to cancel-and-revert on last-observer unsubscribe, discarding warm rosters on interrupted switches) and reverted.

Sink fixes (Rust): rotation is best-effort (a transiently locked rotated generation degrades to an unrotated append instead of dropping every record, with a stderr line); a permanently dead sink warns once in the console instead of being indistinguishable from "no switches traced"; the rotate-once contract is asserted unconditionally in the concurrency test; the degradation test blocks rotation with a non-empty directory so it holds under root and on Windows.

Harness fixes: stale longtask deliveries drained at sample start; settle specs assert the clean-measure contract before their own timing-budget guard and fail truncated-only runs with an explicit harness-timing message (null-safe).

Deferred by design (accepted, not missed — full 32-item ledger travels with the review harness): drop-not-fabricate biases (dropped traces emit no drop record; >3s single-frame renders drop rather than record), population scoping (deep-link and search-hit navigations anchor at goChannel; Pulse startDm untraced), eventCount = raw wire events, no-signal process suspensions bounded by the 30s entry timeout, and the cross-process rotation race mooted by tauri-plugin-single-instance.

Validation on head: desktop unit suite 5,471/5,471; Rust perf_log 11/11 (fmt + clippy clean); settle + navigation smoke specs green; member-heavy perf baseline green with per-direction medians; file-size/px/whitespace gates clean; CI 24 checks passing.

@Maxwellimus
Maxwellimus requested a review from wesbillman August 26, 2026 00:58
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