Skip to content

fix(desktop): stop dropping live events behind the reader clock - #3762

Open
tfournet wants to merge 1 commit into
block:mainfrom
tfournet:fix/live-filter-clock-skew
Open

fix(desktop): stop dropping live events behind the reader clock#3762
tfournet wants to merge 1 commit into
block:mainfrom
tfournet:fix/live-filter-clock-skew

Conversation

@tfournet

Copy link
Copy Markdown

Summary

The desktop channel live subscription derived its since filter from the reader's clock. Nostr created_at is author-assigned, and the relay accepts timestamps up to 900s from server time (MAX_TIMESTAMP_DRIFT_SECS, crates/buzz-relay/src/handlers/ingest.rs). Because since is re-applied at live fan-out (crates/buzz-relay/src/subscription.rsfilters_matchcrates/buzz-core/src/filter.rs), a valid message from a peer whose clock is behind the reader's was dropped — silently, and for the entire lifetime of that subscription. Nothing recovered it except a reconnect or channel re-entry.

The fix removes the reader-clock since and bounds initial replay with limit instead, which does not constrain future fan-out.

Removing since alone would have traded one loss window for another: the CLOSED retry path resent the original filter unchanged, so after a backoff of up to 30s a subscription would resume with only the newest limit events and permanently miss anything older in the gap. This PR therefore pairs the filter change with paged recovery, deriving the resume point from lastSeenCreatedAt — an author timestamp the relay already accepted, which is exactly why it is safe where a reader clock is not. That machinery already existed for reconnect (relayReconnectReplay.ts) and is reused rather than reinvented.

Changes, all desktop TypeScript (no Rust, no relay changes):

  • relayChannelFilters.ts — new buildChannelLiveFilter: no since, limit: 50. The limit must stay above zero: shouldPageReconnectReplay gates paged reconnect recovery on filter.limit > 0, so limit: 0 would silently disable reconnect history recovery. Documented at the call site.
  • relayClientSession.tssubscribeToChannelLive uses the new builder; adds an optional per-flush callback so replay projection can be batched.
  • relayClosedRecovery.tsCLOSED retry now performs paged catch-up from lastSeenCreatedAt - skew when a last-seen timestamp exists. Terminal CLOSED handling (auth/access/filter failure) and rate-limit backoff are unchanged.
  • relayReconnectReplay.ts — replayed events route through the buffered event path rather than bypassing it.
  • hooks.ts — window projection is batched per relay flush and runs only when an event actually mutated the window. Without this, removing since would trigger up to 50 full-store flatten-and-reconcile passes on a single channel switch, since main projects once per event.

Related issue

None found for this bug. Searched open and closed issues and PRs for clock skew, created_at since, live subscription since, message loss subscription, and drops messages.

Related but not duplicate: #3104 ("Desktop thread can render agent reply before triggering message under client clock skew"). Same root cause — author-assigned created_at under clock skew — but the opposite symptom. #3104 is about messages that arrive and sort into the wrong position; this PR is about messages that never arrive at all because the reader's clock acts as a delivery filter. Fixing one does not fix the other.

Adjacent instance not fixed here: buildChannelMentionFilter (relayChannelFilters.ts) still uses since: Math.floor(Date.now() / 1_000) from the reader's clock, so channel mentions from a peer with a lagging clock can be dropped the same way. It is left alone deliberately — it needs its own check on whether a bare limit is safe there and whether that subscription has a recovery path. Happy to fold it into this PR or file a follow-up, whichever the maintainers prefer.

Testing

No UI change, so no screenshots.

New unit coverage in relayChannelFilters.test.mjs:

  • the live filter has no since key (the regression guard for this bug)
  • limit > 0, and shouldPageReconnectReplay(liveFilter) === true — pinning the coupling a future limit: 0 would silently break
  • #h scoping and the kinds set, including KIND_CHANNEL_THREAD_SUMMARY

The existing relay-reconnect.spec.ts:216 ("reconnect backfills more missed channel messages than the live subscription limit") is the end-to-end proof for the recovery half: it injects 260 messages during a disconnect and requires the oldest to remain reachable, which fails if recovery is bounded by the live limit. It passes on this branch.

Local verification (Fedora 44, x86_64):

Check Result
just desktop-check (incl. file-size, px-text, pubkey-truncation gates) pass
just desktop-test 3804 passed, 0 failed
just desktop-typecheck pass
just desktop-build pass
pnpm exec playwright test --project=smoke (all 4 shards) pass

Two disclosures so they are not discovered in review rather than read here:

  1. just ci does not go green on my machine, because of buzz-db's replica_fence::tests::fence_starts_closed_and_opens_on_advance — a nanosecond-vs-microsecond timestamp assertion. It fails identically on unmodified main, and this branch changes zero .rs files (git diff --name-only origin/main..HEAD | grep -c '\.rs$' → 0), so it cannot be caused by this change. Path filtering means Rust Lint and Unit Tests do not run for a desktop-only PR anyway.
  2. relay-reconnect.spec.ts:97 ("failed initial relay dial retries automatically") flaked once under shard parallelism with Relay state seam is not installed — the spec's own instrumentation hook was not yet installed when it polled. It passes in isolation on both main and this branch, and CI runs with retries: 2.

Performance is unchanged, as expected — this is a correctness fix, not a latency change. Cold-switch and warm-switch longtask harnesses are at parity with main within run-to-run spread.

Signed-off-by: Tim Fournet <timfournet@gmail.com>
@tfournet
tfournet requested a review from a team as a code owner July 30, 2026 16:33
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