Skip to content

WorkflowChatTransport treats client abort as reconnectable interruption #1186

@gsathya

Description

@gsathya

Summary

WorkflowChatTransport currently treats a client-side AbortController abort as if it were an accidental stream interruption, so it may automatically reconnect and reopen the HTTP stream.

This makes it hard to intentionally detach the client from the HTTP stream while letting the underlying workflow/agent run continue (which is a key resumable-streams use case).

Expected Behavior

When the caller aborts the transport request:

  • The current HTTP stream should stop.
  • The transport should not auto-reconnect.
  • The underlying workflow/agent run should continue (server-side behavior unchanged).

Actual Behavior

  • Aborting the client request during the initial POST stream (before a finish chunk is received) causes WorkflowChatTransport to treat the abort as an interruption and automatically reconnect.
  • The reconnect GET path does not propagate the caller’s abort signal.

Why This Matters

This issue affects a valid resumable-streams workflow where the client intentionally disconnects only the HTTP stream (without canceling the underlying workflow run).

The intended flow is:

  1. The user deliberately detaches from the current stream (for example: closing a panel, navigating away, or pressing a "disconnect" action).
  2. The workflow/agent keeps running on the server.
  3. The client reconnects later (manually or automatically) and resumes streaming from the last chunk.

In this flow, step 1 is not a failure and should not trigger an immediate reconnect. WorkflowChatTransport currently treats that intentional client abort like an accidental stream interruption, so it can reconnect right away instead of staying disconnected until the client chooses to resume.

Reproduction (conceptual)

const controller = new AbortController();

const transport = new WorkflowChatTransport({
  api: "/api/chat",
  onChatEnd: () => {
    console.log("chat finished");
  },
});

const stream = await transport.sendMessages({
  trigger: "submit-message",
  chatId: "test-chat",
  messages,
  abortSignal: controller.signal,
});

const reader = stream.getReader();

// Start reading, then abort intentionally:
reader.read();
controller.abort(); // intended: detach HTTP stream only

Observe:

  • The transport may reconnect (GET /api/chat/{runId}/stream?startIndex=...) instead of staying detached.

Root Cause

In packages/ai/src/workflow-chat-transport.ts:

  • sendMessagesIterator() catches POST stream errors and (if no "finish" chunk was recorded) falls into reconnect logic.
  • Abort is not distinguished from genuine network interruption in a way that preserves "intentional detach" semantics.
  • reconnectToStreamIterator() reconnect fetch path can drop/ignore the caller abort signal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions