Skip to content

Facet broadcast can reorder consecutive WebSocket frames #2177

Closed

Description

@scott-edwards

Describe the bug

Agent.broadcast() can reorder consecutive WebSocket frames when the Agent is a sub-agent facet and no forwarded invocation bridge is active.

The facet branch calls _cf_broadcastToParentSubAgent(msg, without) without returning or awaiting its promise. Each call can therefore start an independent asynchronous route through the root Agent. Two broadcasts made in order can reach the browser in the opposite order.

This affects streamed chat output. In a production Think sub-agent, a terminal tool-card frame could arrive before the preceding assistant text frame. The client then rendered incomplete output or entered stream recovery even though the exact sub-agent WebSocket stayed open.

PR #2090 added _cf_routeSubAgentConnectionOperation(), which queues send, setState, and close operations per virtual connection. The current facet broadcast() path bypasses that ordered queue.

To Reproduce

  1. Connect a browser WebSocket directly to a chat-capable sub-agent.
  2. Let the forwarded invocation frame finish so _cf_activeSubAgentBridge() is not active.
  3. From the facet, call this.broadcast(frame1) and then this.broadcast(frame2) in the same turn.
  4. Add a delay to the first root broadcast route, or repeat under normal RPC latency.
  5. Observe that frame2 can reach the browser before frame1.

The relevant current path is equivalent to:

broadcast(msg, without) {
  if (this._isFacet) {
    this._cf_broadcastToParentSubAgent(msg, without);
    return;
  }
  // top-level path
}

The downstream fix that resolved the live reproduction routes facet broadcasts through each exact virtual connection:

if (this._isFacet) {
  for (const connection of this.getConnections()) {
    if (without?.includes(connection.id)) continue;
    connection.send(msg);
  }
  return;
}

Virtual connection.send() already uses _cf_routeSubAgentConnectionOperation(). Its per-connection promise tail preserves call order. This also keeps the existing without behavior.

A regression test should verify that:

  • facet broadcast() does not use the aggregate parent broadcast path;
  • two consecutive broadcasts use each exact virtual connection in call order;
  • without still excludes the selected connection;
  • the top-level Agent path does not change.

A downstream patch and regression test are visible in science-of-people/prosocial-v2#664.

Expected behavior

For one WebSocket connection, calls to this.broadcast() must arrive in call order for top-level Agents and sub-agent facets. A later terminal frame must not overtake earlier streamed content.

Screenshots

Not applicable. WebSocket traces and persisted message order were used to diagnose the failure.

Version:

  • agents@0.20.1
  • package commit cc50e404b23ee480df7c783c5fa34c4156950889
  • @cloudflare/think@0.15.1

Additional context

Related upstream work:

This is not a prompt, tool, model, provider, client reconnect, or second-startup-path problem. It is a server-side ordering gap between facet broadcast() and the ordered virtual connection path.

Activity

  1. scott-edwards commented on Aug 28, 2026

    @scott-edwards
    Author

    Closing this report. I checked the published artifact after filing and confirmed that agents@0.22.0 already contains PR #2090, including the ordered facet broadcast barrier and the Think regression that keeps STREAM_RESUMING before a later terminal broadcast. This report was based on our older preview pin plus a partial downstream patch, not the current published SDK. Sorry for the incorrect report.

  2. O4epegb commented on Sep 3, 2026

    @O4epegb

    Are you sure it was fixed? I'm encountering similar issues on 0.22.0 still

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions