Make streaming more resilient in Duo Chat

What does this MR do and why?

Final MR in the chain: wires WorkflowStream into Duo Agentic Chat in place of stream_manager, and surfaces a recoverable error state when automatic reconnection is exhausted.

The service classes themselves land in the three MRs below — this one is the wiring and the UX.

Changes

  • duo_agentic_chat_state_manager.vue — consumes workflowStreamFactory instead of stream_manager. onSocketError now recognises max_retries_exceeded and renders ConnectionErrorAlert rather than a generic failure; handleRetryConnection() clears the state and calls RetryableWorkflowStream.retry().
  • ConnectionErrorAlert (new) — danger alert with a Retry button, shown once all automatic attempts are spent. Follows the credits_exhausted_alert.vue pattern.
  • event_hub.js — subscribes through the stream service.
  • stream_worker.js — parses frames before posting, so consumers receive objects rather than raw JSON strings. Uses WS_CLOSE_NORMAL instead of a bare 1000.
  • workflow_utils.js — follow-on adjustments for the above.

Chain

MR Delivers
1 !249095 (merged) BufferedEventHub
2 !247787 (merged) WorkflowStream
3 !249096 (merged) RetryableWorkflowStream + factory
4 this MR Wiring + retry UX

How to validate locally

No setup is required. A dev-only mock at ee/app/assets/javascripts/ai/duo_agentic_chat/websocket/dws_mock_websocket.js wraps the real WebSocket and lets you synthesise events from the browser console, so chat still talks to your GDK and streams real replies. See its README for details on the tool itself. It is active whenever NODE_ENV is development or test, and becomes available once a socket has been opened.

  1. Open Duo Agentic Chat in your GDK.
  2. Send a prompt to open the socket.
  3. Open the browser console and use gl.dwsMockSocket.
Console call Expected
gl.dwsMockSocket.close(1006) Silent reconnect, no visible error
gl.dwsMockSocket.close(1001) Silent reconnect, no visible error
gl.dwsMockSocket.close(1013) "Already responding in another tab" state, no reconnect
gl.dwsMockSocket.close(1008) Out-of-credits or billing-forbidden state, no reconnect
gl.dwsMockSocket.close(1000) Stream stops quietly
gl.dwsMockSocket.message('{"newCheckpoint":{"status":"running"}}') Delivers a frame, which resets the retry counter
gl.dwsMockSocket.open() Synthesises an open event on the current socket
gl.dwsMockSocket.queue({ close: 1006, times: 3 }) Answers the next 3 connections with a close
gl.dwsMockSocket.clearQueue() Drops the queue, so connections reach the real server

Reload the page for a fresh connection.

Exhausting the retries

Closing by hand four times does not work: each reconnect reaches the real Duo Workflow Service, gets a real reply, and any delivered frame resets the retry counter. Queue the answers for the reconnects instead:

gl.dwsMockSocket.queue({ close: 1006, times: 3 }); // answer the next 3 reconnects
gl.dwsMockSocket.close(1006);                      // start the cascade

MAX_WS_RETRIES is 3, so three reconnects are attempted and the fourth consecutive failure gives up and shows ConnectionErrorAlert. Delays are exponential with jitter: roughly 0.5-1s, then 1-2s, then 2-4s, so the whole cascade takes a few seconds.

Once the alert appears, the Retry button resets the counter and reconnects immediately, with no backoff. The queue is spent by then, so that reconnect reaches the real service and the chat carries on. gl.dwsMockSocket.clearQueue() drops anything still queued.

Watch the console for breadcrumbs: every close logs [duo-chat][stream] closed with the code, category, and whether it is retryable, and each reconnect attempt logs [duo-chat][stream] reconnecting with the trigger, consecutive-failure count, max, and delay.

gl.dwsMockSocket.error() is a no-op here, because the stream worker never assigns an onerror handler. Use close(1006) instead to reach the error path, which is also what a dropped connection looks like.

FYI: Reconnect button is not expanded and centered anymore

It appears like that in the video but I changed it to left aligned based on @nickleonard's suggestion.(I don't want to record the video again):

Screenshot 2026-08-14 at 15.10.25.png

MR acceptance checklist

  • Changelog trailer (Changelog: added, EE: true)
  • Tests updated for the new error path
  • i18n strings wrapped with s__() and PO file updated
  • Demo video — to be attached by author

References

Make Duo Chat WebSocket stream more resilient a... (#605829 - closed)

Edited by Enrique Alcántara

Merge request reports

Loading
Loading