Scope Duo chat stream adoption to the workflow the socket belongs to

What does this MR do and why?

The Duo Agentic Chat websocket stream is factory-scoped, and the chat state manager adopted a connected stream purely on connection status — it could not tell which workflow the open socket belonged to. Because of that, the history view (and onNewChat() via cleanupState()) had to disconnect() the socket before navigating anywhere else. Workhorse translates that close (code 1000) into stopWorkflow, so a user who started a long reply and then opened a blank chat or another thread killed the reply they were waiting on. Every such navigation also inflates the workflow execution stopped ratio SRE now watches after the INC-13926 incident.

This MR scopes stream adoption to a workflow id:

  • WorkflowStream records the workflowID from the start request it was connected with and exposes it via getStatus(). Reconnects reuse the same initial message, so the id survives retries.
  • The state manager adopts a connected stream only when its workflow id matches the active thread. A foreign stream is left alone (and not subscribed to, so its checkpoints cannot splice into another thread's transcript) — the in-flight turn keeps running server-side and is picked up from the checkpoint (and the buffered live chunks) when the user returns.
  • The unconditional disconnect() calls are removed from the history view's onNewChat() / onThreadSelected() and from cleanupState(). The socket is now closed only in the two places that represent a deliberate stop: the user cancelling the turn (onChatCancel), and startWorkflow() reconnecting for a user-initiated turn.
  • beforeDestroy keeps the shared transcript only when the connected stream carries the active thread's own reply.

Known trade-off (accepted in the issue): with the single-socket factory, viewing a second concurrently-running thread does not live-stream while another thread holds the socket; it hydrates from the checkpoint and connects on the next user turn.

References

Screenshots or screen recordings

No visual changes — this changes websocket lifecycle behavior only.

How to set up and validate locally

  1. Enable Duo Agentic Chat locally (GDK with Duo Workflow / AI gateway set up).
  2. Open the Duo panel, start a chat, and send a prompt that produces a long-running reply.
  3. While the reply is streaming, open the History tab and start a new chat (or open another thread).
  4. Before this MR: the reply's workflow is stopped server-side (workflow execution stopped in DWS). After this MR: the socket stays open.
  5. Return to the original thread from History: the transcript is restored and the still-streaming reply continues live.
  6. Cancelling a turn or sending a message in a different thread still stops the previous turn (deliberate stops are unchanged).

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading
Loading