Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/usechat-ws-reconnect-resync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@aws-blocks/bb-agent": minor
---

useChat now survives a mid-turn Realtime WebSocket disconnect/reconnect and send-path failures.

Long-running agent turns (up to 8h on AgentCore) can outlive API Gateway's WebSocket limits (2h max connection, 10-min idle). Previously useChat subscribed once and assumed the socket stayed healthy for the whole turn, so a reconnect gap could swallow the `done` chunk and leave `loading` stuck true, and a rejected/timed-out send (e.g. a 504 cold dispatch) left the spinner hanging with an orphaned empty assistant bubble.

- The `subscribe` option now accepts an options object (`{ onMessage, onDisconnect?, onReconnect? }`) in addition to a bare handler — backward compatible.
- On reconnect, useChat re-syncs authoritative state from the database (`getConversation` to recover the final assistant text if the turn completed during the gap; `getPendingInterrupts` to recover a missed interrupt). If the turn is still running, loading is preserved and streaming resumes on the resubscribed channel.
- `sendMessage` and `respondToInterrupt` now reset `loading` and surface `onError` when the underlying RPC rejects, dropping any empty placeholder.
- A bounded failsafe clears `loading` if no terminal chunk arrives within a window after a reconnect, so the spinner can never hang indefinitely.
14 changes: 10 additions & 4 deletions packages/bb-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,9 +724,12 @@ const chat = useChat({
getConversation: (id) => api.getConversation(id),
resume: (chId, responses, convId) => api.resume(chId, responses, convId),
},
subscribe: async (channelId, handler) => {
// Forward the subscribe argument (`sub`) VERBATIM to the channel — it is an
// options object carrying onMessage/onReconnect/onDisconnect. Passing only a
// bare handler would drop the reconnect callbacks the transport needs.
subscribe: async (channelId, sub) => {
const channel = await api.getChannel(channelId);
return channel.subscribe(handler);
return channel.subscribe(sub);
},
onMessagesChange: (msgs) => renderMessages(msgs),
onLoadingChange: (loading) => updateSpinner(loading),
Expand Down Expand Up @@ -788,9 +791,12 @@ const chat = useChat({
createConversation: () => api.createConversation(userId),
getConversation: (id) => api.getConversation(id),
},
subscribe: async (channelId, handler) => {
// Forward the subscribe argument (`sub`) VERBATIM to the channel — it is an
// options object carrying onMessage/onReconnect/onDisconnect. Passing only a
// bare handler would drop the reconnect callbacks the transport needs.
subscribe: async (channelId, sub) => {
const channel = await api.getChannel(channelId);
return channel.subscribe(handler);
return channel.subscribe(sub);
},
onMessagesChange: (msgs) => renderMessages(msgs),
onLoadingChange: (loading) => updateSpinner(loading),
Expand Down
Loading
Loading