fix(bb-agent): useChat survives mid-turn WebSocket reconnect and send-path failures - #501
Open
soberm wants to merge 3 commits into
Open
fix(bb-agent): useChat survives mid-turn WebSocket reconnect and send-path failures#501soberm wants to merge 3 commits into
soberm wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: 8dd6535 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
soberm
force-pushed
the
fix/usechat-ws-reconnect-resync
branch
from
September 7, 2026 13:09
2e1a526 to
a1a0c56
Compare
soberm
marked this pull request as ready for review
September 7, 2026 13:11
soberm
added a commit
that referenced
this pull request
Sep 7, 2026
…nc reaches the transport Address PR #501 review (BLOCKER + HIGH + MEDIUM): useChat built its subscribe arg as a callable-with-props (Object.assign(fn, {onMessage,onReconnect})). Both bb-realtime middlewares resolve subscribe with `typeof arg === 'function'` FIRST and treat any function as a bare handler, never reading its properties — so onReconnect/onDisconnect were silently dropped and the DB re-sync + bounded failsafe (this PR's headline stuck-spinner fix) never fired on the real transport for the documented adapter usage. - index.hooks.ts: pass a plain ChatSubscribeOptions object; the function-first middleware now hits its object branch and reads onReconnect. subscribe param type still accepts a bare handler OR options (back-compat). - index.test.ts: the subscribeCapture shim was options-first (read props off a function), the reverse of the real middleware, so every reconnect test passed vacuously. Rewrote it to mirror the real function-first precedence — a function is always a bare handler — so a regression to a callable now fails the tests. - README.md: both useChat adapter snippets now forward the subscribe argument (`sub`) verbatim to channel.subscribe, with a note that a bare handler drops the reconnect callbacks. bb-agent 114/114, build clean, biome 0 errors, cast-free.
…-path failures
Long agent turns (up to 8h on AgentCore) can outlive API Gateway WebSocket
limits (2h connection, 10-min idle). useChat subscribed once and assumed the
socket stayed healthy, so a reconnect gap could swallow the done chunk (loading
stuck true forever) and a rejected/504 send left a hung spinner + orphaned
empty assistant bubble.
- Widen the subscribe option to accept { onMessage, onDisconnect?, onReconnect? }
(backward compatible with the bare-handler form).
- On reconnect, re-sync authoritative state from the DB: getConversation to
recover the final assistant text if the turn completed during the gap, and
getPendingInterrupts to recover a missed interrupt. If the turn is still
running, keep loading and resume streaming on the resubscribed channel.
- Wrap api.sendMessage / api.resume in try/catch: reset loading, drop the empty
placeholder, and surface onError on rejection.
- Bounded post-reconnect failsafe clears loading if no terminal chunk arrives
within a window, so the spinner can never hang indefinitely.
Adds unit tests for send-path reset, reconnect re-sync (done missed / still
running), pending-interrupt re-check, and the bounded failsafe.
…turn-identity guard, single onError Resolve code-review findings on the useChat reconnect/re-sync path: - Failsafe no longer misfires on legitimately long/silent turns: any received chunk re-arms the post-reconnect failsafe, so it only fires after a window of COMPLETE silence, and the window is raised to 11 min (RECONNECT_FAILSAFE_MS = 660_000) to sit above the Realtime 10-min idle-timeout + reconnect budget. - Reconnect re-sync can no longer clobber correct text or adopt a stale/previous turn: assistantId is nulled on done/error (reliable in-flight signal), and handleReconnect captures turnAtStart before the getConversation await and only adopts the persisted text when it is still the same in-flight turn AND the persisted content extends what was already streamed. - A send-path rejection that may have started the turn server-side no longer double-reports: onError is guarded to fire at most once per turn (reportError), matching the error-chunk path. - The widened subscribe arg is a callable that also carries onMessage/onReconnect/onDisconnect, so existing adapters that invoke arg2 keep working while new adapters can read the reconnect hooks. - Teardown guard: destroy() sets `destroyed`, and handleReconnect/armFailsafe early-return so no callback fires after unmount. Tests updated/added for failsafe liveness, live-done-vs-late-getConversation, stale-snapshot rejection, and single-onError.
…nc reaches the transport Address PR #501 review (BLOCKER + HIGH + MEDIUM): useChat built its subscribe arg as a callable-with-props (Object.assign(fn, {onMessage,onReconnect})). Both bb-realtime middlewares resolve subscribe with `typeof arg === 'function'` FIRST and treat any function as a bare handler, never reading its properties — so onReconnect/onDisconnect were silently dropped and the DB re-sync + bounded failsafe (this PR's headline stuck-spinner fix) never fired on the real transport for the documented adapter usage. - index.hooks.ts: pass a plain ChatSubscribeOptions object; the function-first middleware now hits its object branch and reads onReconnect. subscribe param type still accepts a bare handler OR options (back-compat). - index.test.ts: the subscribeCapture shim was options-first (read props off a function), the reverse of the real middleware, so every reconnect test passed vacuously. Rewrote it to mirror the real function-first precedence — a function is always a bare handler — so a regression to a callable now fails the tests. - README.md: both useChat adapter snippets now forward the subscribe argument (`sub`) verbatim to channel.subscribe, with a note that a bare handler drops the reconnect callbacks. bb-agent 114/114, build clean, biome 0 errors, cast-free.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Issue #, if available: internal — useChat WebSocket timeout handling for long agent turns
useChat(packages/bb-agent/src/index.hooks.ts) subscribes to the Realtime channel once per turn and assumes the subscription stays healthy for the whole turn. After the AgentCore migration a turn can run up to 8h, while the Realtime BB's API Gateway WebSocket caps at 2h connection duration and a 10-min idle timeout. So:donechunk lands in the gap,loadingstaystrueforever (spinner never clears).sendMessage/respondToInterruptawaitthe RPC with notry/catch; a rejected or 504-timed-out cold dispatch leavesloadingstuck and an orphaned empty assistant bubble.Stacked on #497 (PR 2 of 2). PR1 makes the production Realtime transport auto-reconnect + resubscribe and adds
onReconnect; this PR consumes those primitives inuseChatand adds a DB-backed recovery contract. Base branch isfix/realtime-prod-reconnect— review/merge #497 first.Changes
subscribecontract (backward compatible): the option now accepts{ onMessage, onDisconnect?, onReconnect? }in addition to a bare handler, mirroring bb-realtime'sSubscribeOptions.getConversationrecovers the final assistant text if the turn completed during the gap;getPendingInterruptsrecovers a missed interrupt. If the turn is still running,loadingis preserved and streaming resumes on the resubscribed channel.try/catcharoundapi.sendMessage/api.resumeresetsloading, drops the empty placeholder, and surfacesonErroron rejection/504.done/error/interruptarrives within a window after a reconnect,loadingis cleared (+onError) so the spinner can never hang indefinitely. Timer cleared on terminal chunk / unmount.Validation
packages/bb-agent/src/index.test.ts: send-path rejection resets loading + onError (sendMessage and respondToInterrupt); reconnect re-syncs final assistant text whendonewas missed; reconnect while turn still running keeps loading true; reconnect re-checks pending interrupts; bounded failsafe clears loading with no terminal chunk (mock timers).index.test.js111/111 pass (6 new + existing useChat suite);tsc --buildclean;biome lint0 errors; cast-free.index.cdk.test.jshas 4 pre-existingCannotFindAssetfailures (sibling-package build-artifact ordering) unrelated to this change — reproduced on baseline with changes stashed.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.