Skip to content

Live thread-summary push: badge counts update on reply ingest#1521

Merged
tlongwell-block merged 2 commits into
mainfrom
eva/live-thread-summaries
Jul 6, 2026
Merged

Live thread-summary push: badge counts update on reply ingest#1521
tlongwell-block merged 2 commits into
mainfrom
eva/live-thread-summaries

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two badge symptoms reported on staging (thread: buzz-relay-optimization), one root cause:

  • Frozen live counts — the reply count on a thread's timeline row never increments while replies arrive live; it only updates after reconnect/re-entry/scrollback.
  • Missing badges — rows that reach the screen via the live overlay or refetch reconciliation show no thread badge at all until scrollback re-delivers them as page rows.

Mechanism (full trace in RESEARCH/THREAD_COUNT_BADGE_MECHANISM_2026_07_05.md on my workspace, summarized in the PR thread): badge counts come exclusively from relay-signed kind:39005 overlays attached to channel-window page responses — a snapshot frozen at fetch time. thread_metadata is already updated transactionally on every reply insert, but the relay never told anyone. Live replies deliberately skip the main timeline store, so the client can't compensate locally.

Fix: the relay pushes what it already knows

Relay (side_effects.rs, call sites in ingest.rs + delete handlers):

  • After a reply insert commits — and after threaded deletes decrement counters — sign a fresh 39005 from thread_metadata and broadcast it through the existing Redis + local fan-out paths (same access gating as every other event).
  • Same tags/content contract as the page overlay in api/bridge.rs: one contract, two delivery doors.
  • Never stored. Pages recompute summaries from thread_metadata on every fetch; a persisted copy would only add staleness. 39005 is relay-signed, so clients can't spoof counts.
  • Counts are re-read post-commit, not incremented, so pushes are exact under concurrent replies/deletes, and deletes count down.
  • Spawned fan-out — zero added latency on the ingest acknowledgement.

Desktop:

  • The channel live subscription (subscribeToChannelLive) adds kind 39005 — scoped to this one subscription, not CHANNEL_EVENT_KINDS, so unread tracking and timeline caches never see summary overlays.
  • Window store gains a liveSummaries overlay: newest created_at wins per root; cleared by the authoritative head refetch; survives scrollback pages (a racing push can be fresher than a just-fetched page summary). Merged over page summaries in channelWindowThreadSummaries — which also fixes the missing-badge symptom for rows that never got a page summary.
  • The existing local reply merge in useLiveChannelUpdates is intentionally kept: badge rendering resolves local-vs-relay counts via max() in mergeThreadSummaries, and without the local path a new client against an old relay (no 39005 push) would freeze counts entirely. (An earlier revision of this branch removed it and promptly broke the thread-badge smoke suites — CI caught it; reverted.)

Backward compatible both directions: old clients ignore the pushed 39005 (parameterized-replaceable, never stored); new clients against an old relay degrade to today's fetch-time snapshot behavior.

Mobile (post-#1518 channel windows) has the same gap — follow-up, not this PR.

Validation

  • Ignored e2e test_reply_ingest_pushes_live_thread_summary, run against a real local relay: subscribe with the desktop-shaped filter, reply → pushed summary with reply_count: 1, delete the reply → pushed summary with reply_count: 0. Both received live, no window refetch.
  • Window-store unit tests: overlay wins over page summary, badge for page-less roots, stale-push rejection (identity-stable), head-refetch clearing, scrollback survival.
  • Response-parser unit tests: well-formed parse + malformed/mistargeted drops.
  • cargo test -p buzz-relay: 463 passed. cargo clippy --tests: clean.
  • Desktop typecheck / check / test (1623): all green; thread-badge smoke suites (messaging:561, thread-unread, thread-reply-anchor-roleplay, human-edit-agent-content, spoiler:145) pass locally against the built bundle.

Also cherry-picks the ChannelScreen size-gate trim from #1520 (main's Desktop CI is currently red without it; drops out on rebase if #1520 merges first).

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 2 commits July 5, 2026 14:26
#1490 and #1487 each landed a few lines in ChannelScreen.tsx and pushed
it to 1006, tripping the 1000-line file-size check and turning main's
Desktop CI red. Recover the headroom by tightening prose comments (no
semantic content dropped) and compacting one ternary the formatter
accepts on a single line. No code behavior changes; 999 lines by the
checker's count.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Thread badge counts in the channel timeline came only from relay-signed
kind:39005 overlays attached to channel-window page responses — a
snapshot frozen at fetch time. Live replies route to the thread-replies
cache and never become timeline rows, so an active thread's count never
moved until something refetched the head window; rows that reached the
screen outside a page (live overlay, refetch reconciliation) had no
badge at all.

Relay: after a reply insert commits (and after threaded deletes
decrement counters), sign a fresh 39005 from thread_metadata — same
tags/content contract as the page overlay in api/bridge.rs — and fan it
out via the existing Redis + local paths. Never stored: pages recompute
summaries on every fetch, so a persisted copy would only add staleness.
Counts are re-read post-commit, not incremented, so pushes are exact
under concurrency and deletes count down.

Desktop: the channel live subscription adds kind 39005; the window
store gains a liveSummaries overlay (newest created_at wins per root,
cleared by the authoritative head refetch) merged over page summaries
in channelWindowThreadSummaries. The existing local reply merge in
useLiveChannelUpdates is kept unchanged: without it, a new client
against an old relay (no 39005 push) would freeze counts entirely, and
badge rendering already resolves local-vs-relay via max() in
mergeThreadSummaries.

Validation: window-store + response-parser unit tests; ignored e2e
(test_reply_ingest_pushes_live_thread_summary) verified against a local
relay — reply pushes reply_count=1, deleting it pushes 0; cargo test -p
buzz-relay (463 passed); desktop typecheck/check/test all green; smoke
suites covering thread badges (messaging:561, thread-unread,
thread-reply-anchor-roleplay) pass against the built bundle.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block tlongwell-block force-pushed the eva/live-thread-summaries branch from c335ea4 to 98d5204 Compare July 5, 2026 23:24
@tlongwell-block tlongwell-block merged commit 74a30c3 into main Jul 6, 2026
50 of 52 checks passed
@tlongwell-block tlongwell-block deleted the eva/live-thread-summaries branch July 6, 2026 14:25
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.

1 participant