Skip to content

fix(desktop): bound thread /query and surface load errors, not false-empty - #6447

Open
wpfleger96 wants to merge 1 commit into
mainfrom
duncan/thread-load-flake-fix
Open

fix(desktop): bound thread /query and surface load errors, not false-empty#6447
wpfleger96 wants to merge 1 commit into
mainfrom
duncan/thread-load-flake-fix

Conversation

@wpfleger96

Copy link
Copy Markdown
Member

Long threads in the desktop app sometimes never load (stuck on a skeleton until you close and reopen the panel), and a failed load silently renders as "No replies in this branch yet" — presenting a broken fetch as an authoritative empty thread with no way to recover. This fixes both, the two IMPORTANT findings from the thread-load investigation.

Defect 1 — unbounded /query request

The shared reqwest::Client in relay.rs sets no timeout, and neither /query request builder set a per-request .timeout(...). A stalled or half-open connection (headers or body never arrive) leaves the request pending forever, so a thread-history load hangs on the skeleton indefinitely.

Fix: a 30s per-request deadline on both /query builders. Scoped per-request rather than client-level because the same client also serves STT/TTS model downloads, builderlab auth, and the media proxy — a client-level timeout would cut those off. The deadline sits above the 25s WS HISTORY_TIMEOUT_MS so a slow-but-live relay isn't cut off before the WebSocket path would be. A timeout surfaces through classify_request_error as the stable "relay unreachable: request timed out" string.

Defect 2 — terminal error painted as empty

ChannelScreen consumed only isPending/data from the thread-replies query. Once React Query exhausted its one retry, isPending was false and the zero-length data fell through selectDeferredListRenderState to the "empty" state — indistinguishable from a genuinely empty branch, with no retry affordance.

Fix: plumb isError + refetch through ChannelScreenChannelPaneMessageThreadPanel. A pure selectThreadRepliesSurface helper decides the paint in strict precedence — the load-bearing invariant is that a terminal error never resolves to "empty", and cached replies stay visible non-destructively under a later error (the error card only surfaces when there is nothing to show). The panel renders an explicit "Couldn't load replies" + Retry card (testids message-thread-replies-error / message-thread-replies-retry).

Tests

  • stalled_query_request_times_out_with_classified_error — a loopback server that never responds; asserts is_timeout() and the stable classified string.
  • selectThreadRepliesSurface — pending→skeleton, terminal error→error (never empty), page-2 failure never empty, cached rows stay visible under error, successful-empty→empty, retry-success→list, streaming→pending, and huddle-transcript collapse.
  • selectThreadRowHighlight — branch-range membership, direct-child, and guide-suppression cases for the extracted highlight helper.

Structure

To stay under the desktop file-size ratchet (both relay.rs and MessageThreadPanel.tsx were one line under the 1000-line ceiling), relay.rs's inline test module moved to relay/tests.rs, and two pure pieces were extracted from the panel: the empty/error reply cards (MessageThreadReplyState) and the per-row branch-highlight derivation (selectThreadRowHighlight).

…empty

Long threads sometimes never loaded (permanent skeleton) or silently
rendered a failed fetch as "No replies in this branch yet". Two defects:

- The shared reqwest client sets no timeout, so a stalled/half-open
  /query HTTP request hangs forever. Add a 30s per-request deadline on
  both /query builders (scoped per-request, not client-level, because
  the client also serves STT/TTS downloads, builderlab auth, and the
  media proxy). Set above the 25s WS history timeout so a slow-but-live
  relay is not cut off early. Timeouts classify to the stable
  "relay unreachable: request timed out" string.

- ChannelScreen consumed only isPending/data; a terminal error fell
  through to the empty state with no recovery. Plumb isError + refetch
  through to MessageThreadPanel and paint an explicit "Couldn't load
  replies" + Retry card. A pure selectThreadRepliesSurface helper pins
  the precedence so a terminal error never resolves to empty and cached
  rows stay visible non-destructively under a later error.

To stay under the desktop file-size ratchet, move relay.rs's inline
test module to relay/tests.rs and extract the reply empty/error cards
(MessageThreadReplyState) and the per-row branch-highlight derivation
(selectThreadRowHighlight) out of the panel, each with unit coverage.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 21, 2026 02:20
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