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

fix(relay): probe the live socket on wake, focus and network - #2958

Merged
baxen merged 2 commits into
mainfrom
art/wake-ping
Nov 7, 2026
Merged

baxen merged 2 commits into
mainfrom
art/wake-ping

Conversation

@baxen

@baxen baxen commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

After the laptop sleeps, the live WebSocket often dies but still looks open. Today the app only notices at the next idle probe, which runs once a minute and then waits 10 s. So live updates can stop for up to ~70 s after you open the lid, with nothing on screen. This PR checks the socket as soon as the app wakes.

Cause

Wake, focus, visibility and network-online already call recovery.retryNow(). That only retries reads that have already failed. A socket that looks connected isn't failed, so nothing ran until the 60 s recovery tick sent its probe.

Change

  • LiveSubscription.check() (live.ts): an admitted socket sends the existing silence probe right away. If no probe answer comes within 5 s, it reconnects through the existing unresponsive path, the same one the 60 s probe uses.
    • Only an answer to a probe counts. Any other frame handled after wake may have been queued before the sleep. That's why the check has its own deadline and doesn't reuse the shared deadline, which any frame clears.
    • It sends nothing if a probe was answered in the last 15 s (focus can fire often), if a check is already pending, or while the relay has asked us to wait (cooldown).
  • session.checkLive() forwards the check. service.ts calls it next to retryNow() from the same wake/focus/online/visibility handlers.
  • The dev broker transport has no check and is unchanged. Desktop and web use subscribeRelayTraffic.

The backoff, the 60 s idle probe and its timeout are unchanged. Morgan vetoed changing the heartbeat timing.

Validation

  • New tests in live.test.ts:
    • A silent socket is replaced 5 s after a check. A non-probe frame doesn't count as an answer, and a second check while one is pending sends nothing.
    • An answering socket is kept. Cooldown and fresh-answer skips work.
  • New test in service.test.ts: a wake (a clock jump between wake ticks) calls check.
  • vitest run src/features/relay/ passes: 143 files, 3212 tests. tsc --noEmit is clean.
  • Not yet done: a real unplugged lid-close on the desktop app, confirming from the relay-ws log that a dead socket is replaced within ~5 s of wake. Draft until that's checked.

Context: this is the wake fix from the connection-status investigation, rewritten on main. The original was Carl's, on Morgan's branch, which isn't on origin. It's independent of the quiet reconnect toast PR (mbot/quiet-reconnect-toast), and the two touch no files in common.

After sleep the live WebSocket often dies without a close and still
looks open. The app only found out at the next idle probe (60 s plus a
10 s timeout), so live updates could stop for up to ~70 s after wake with
nothing shown.

Wake, focus, visibility and online now also ask the live socket to
answer a probe. If no probe answer arrives within 5 s, the socket is
replaced through the existing reconnect path. Only a probe answer
counts: other frames handled after wake may have been queued before the
sleep. A probe answered in the last 15 s skips the check, a pending
check is not repeated, and a relay cooldown sends nothing.

Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
Signed-off-by: Bradley Axen <baxen@squareup.com>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 LGTM. Probe-only proof, own deadline, cooldown/fresh skips, and teardown on reconnect/dispose all check out. Non-blocking: the 15 s fresh-answer skip also suppresses the online hint, so a network change shortly after a focus check falls back to the 60 s probe. Still needs the real lid-close test before leaving draft.

Review feedback: the 15 s fresh-answer skip also suppressed the online
hint, so a socket killed by a network change shortly after a focus check
waited for the 60 s probe. The online event now always probes.

Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen
baxen marked this pull request as ready for review November 7, 2026 04:36
@baxen
baxen requested a review from a team as a code owner November 7, 2026 04:36
@baxen
baxen merged commit ae9efa2 into main Nov 7, 2026
62 checks passed
@baxen
baxen deleted the art/wake-ping branch November 7, 2026 04:36
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T04:40:29.073425Z a46ed12 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a46ed12df4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (closed || !authenticated || checkDeadline || admission.delay())
return;
if (!network && Date.now() - answeredAt < LIVE_CHECK_FRESH_MS) return;
sendProbe();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Jitter wake probes before sending them

When online fires after a shared network blip, the handler in service.ts synchronously reaches this REQ, so the entire client fleet probes the relay at once and, five seconds later, can begin reconnecting in the same narrow window. Add bounded jitter before the probe while retaining the existing coalescing and capped reconnect backoff; the repository explicitly requires relay-facing recovery paths to account for fleet-wide reads and jitter their start.

AGENTS.md reference: AGENTS.md:L149-L161

Useful? React with 👍 / 👎.

// A socket not yet admitted has its own deadline; a cooldown forbids requests.
if (closed || !authenticated || checkDeadline || admission.delay())
return;
if (!network && Date.now() - answeredAt < LIVE_CHECK_FRESH_MS) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate probe freshness across suspension

When the machine resumes after a short sleep under WAKE_GAP_MS and focus or visibility is the only emitted hint, resume() passes false, so this accepts a probe reply from before suspension as proof that the socket is still alive. A socket can die during that suspension, leaving live updates stalled until the periodic recovery tick; wake/resume signals need to invalidate pre-suspension freshness rather than returning here.

Useful? React with 👍 / 👎.

tlongwell-block added a commit that referenced this pull request Nov 7, 2026
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>

* origin/main:
  Build sidebar headings as one fluid design-system row (#2686)
  fix(messages): swap bubble fills for reactions you've made, drop the ring (#2949)
  fix(messages): align line-start mention capsules with each other (#2899)
  fix(relay): probe the live socket on wake, focus and network (#2958)
  fix(messages): size thread avatars like the timeline's (#2890)
  build(desktop): share the bundled buzz CLI build across worktrees (#2894)
  fix(messages): render review comment mentions as chips and open at the reader's place (#2965)
  feat(agents): read owner messages and canvases over MCP (#2881)

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
baxen added a commit that referenced this pull request Nov 7, 2026
…oser (#2968)

## Summary

The "Reconnecting to relay…" notice showed up too often and too loudly.
It appeared 3 s after any background read failed, as a warning strip
with **Retry now** above the composer, and the same failure was often
repeated in the timeline and the agents banner. Most of those moments
were a slow background read, not a real disconnect, and the app already
retries on its own (1, 2, 4, 8, 16 s, then every 30 s forever, plus
immediately on wake, focus and network-online).

Now:

- **One quiet notice on every page.** Relay recovery shows a single
bottom-right toast reading "Buzz is having trouble connecting" with the
description "Retrying automatically." It has no button and the calm
`info` tone.
- **It waits 30 s** (`RECOVERY_NOTICE_DELAY_MS`, up from 3 s). Blips,
laptop wake and failures that the first retries fix stay silent.
- **No duplicates.** The inline strip in `LiveStatus` is gone. While
recovery is running, the timeline's history-edge error text stays
hidden; it comes back if the failure outlasts recovery. **Retry older
messages** stays either way, because recovery does not retry older
history and paging stops on a failed read.
- **Toasts never cover Send.** The composer and the preview's Join
notice register with a new `useToastClearance(ref)` in `Toast.tsx`, and
a CSS rule lifts the toast stack above them. This applies to every
toast, not only this one. Pages with no composer keep the usual
bottom-right spot.

- **A retry that works clears its old error.** A failed `stream-retry`
request used to leave its error on the live snapshot until the next
stream status, so the notice could stay stuck after a later retry
succeeded. `broker-live.ts` now clears only that request's own error,
and leaves a newer failure in place. Taken from Taylor's #3000.

Unchanged: the recovery backoff, the retry signals, `retryNow` wiring
(wake/focus/online still call it), and "Live updates limited" with its
Retry for failures that recovery does not retry (denials, partial
roster).

Decided in the Buzz thread with baxen: toast on every page, a longer
grace period, simple states, no button, and a toast that keeps the
composer clear. Related: #2958 (wake ping) is independent and touches
different files.

## Screenshots

Taken at 1440 px (light and dark) and 390 px, with a draft typed and
Send clickable. They're posted in the Buzz thread where this was
decided.

## Tests

- Vitest:
- `RecoveryNotice`: quiet until exactly 30 s, then one buttonless notice
that clears once recovery settles.
- `LiveStatus`: empty while recovery runs or shows; focus hands off when
the notice replaces a focused Retry.
- `ChannelTimeline`: the edge error text is hidden while recovering, and
Retry older messages stays, in button and scroll modes. Fails without
the fix.
- `broker-live`: a successful retry clears its earlier control error,
and keeps a newer stream failure. The case without the newer failure
fails without the fix.
- Browser journeys, migrated from the inline strip + Retry now to the
toast. Controlled clocks jump the 30 s delay.
- `relay-recovery`: quiet, then one notice, with no Retry buttons and no
"Relay request failed" anywhere. Recovers with no click.
- `live-status`: the notice sits above the composer and Send stays
clickable at 390/800/1440 px. A long relay reason at enlarged text stays
out of the notice. The notice sits above the preview's Join button. An
empty preview recovers with no Retry. These layout cases **fail without
the clearance CSS** (checked by removing it).
- `live`: quota catch-up and the empty-roster recovery stay quiet inside
the delay and recover with no click.
- `focus-navigation`: the covered-composer F6 case pins clearance to 0
so that path stays exercised.
- `history-loading`, `presence`, `navigation-groups`,
`websocket-actions.profile`: locator updates.
- Run in Chromium and WebKit: `relay-recovery`, `live-status`, `live`,
`history-loading`, `presence`, `navigation-groups`, plus the
toast-touching specs (`focus-navigation`, `message-actions`,
`composer-attachments`, `profiles`, `reactions`, `updates`, `startup`,
…).

Browser cases removed: the Retry-now keyboard and focus cases in
`live-status`, because the control no longer exists. The layout contract
they guarded (notice never blocks the composer) is kept as the
toast-above-composer assertions.

## Deferred

- No human test yet in the desktop app with a real network drop.
- Composers inside dialogs also lift the stack while mounted. That's
harmless but untested.
- `relay-recovery` failed 2 of about 60 local Chromium runs. Both times
the notice cleared before its entrance finished, which suggests recovery
settled while the fixture's HTTP outage was still on. I couldn't
reproduce it with debug logging on a heavily loaded machine. Watch it in
CI.

---------

Signed-off-by: Bradley Axen <baxen@squareup.com>
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants