Skip to content

Fix delayed sub-agent WebSocket operations - #2090

Open
ben-reitz wants to merge 5 commits into
fix-1991-facet-rpc-repliesfrom
fix-2055-subagent-continuation-resume
Open

Fix delayed sub-agent WebSocket operations#2090
ben-reitz wants to merge 5 commits into
fix-1991-facet-rpc-repliesfrom
fix-2055-subagent-continuation-resume

Conversation

@ben-reitz

@ben-reitz ben-reitz commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2027. Fixes #2055.

Why

A sub-agent's virtual WebSocket Connection lives longer than the Workers RPC bridge used to forward one browser frame. Delayed work could therefore call connection.send() through a bridge that had already been disposed.

Think exposed this when STREAM_PENDING arrived but the later STREAM_RESUMING send was dropped, leaving the client waiting for its 60-second timeout. #2027 fixes which bridge an in-flight callable reply uses; this PR handles connection activity after that frame has finished.

What changed

  • Track the live bridge for each forwarded invocation and invalidate it when the invocation ends.
  • Route delayed send, setState, and close calls through the durable root Agent, in call order, with failures reported.
  • Preserve the upstream broadcast capability for active nested frames and use the root when a relay has no live bridge.
  • Add real WebSocket regressions for delayed delivery, state, close, ordering, failure recovery, and fresh-context broadcasts.

Validation

  • pnpm run check
  • Agents tests: 2,488 passed
  • Reporter's Think reproduction passes in direct and sub-agent modes with STREAM_PENDING → STREAM_RESUMING → done

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc50e40

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2090

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2090

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2090

hono-agents

npm i https://pkg.pr.new/hono-agents@2090

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2090

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2090

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2090

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2090

commit: cc50e40

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +7251 to +7255
const activeBridge = this._cf_activeSubAgentBridge(connectionId);
if (activeBridge) {
operation(activeBridge);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Sub-agent messages can reach the browser out of order when a new client frame arrives while earlier messages are still being delivered

Connection operations taken while a client frame is in progress are executed immediately (operation(activeBridge) at packages/agents/src/index.ts:7253) even when earlier operations for the same connection are still waiting in the delivery queue, so a newer message can overtake and reach the browser before an older one.
Impact: A client can receive sub-agent messages, state updates, or a close in the wrong order, e.g. a reply from a new request arriving before a still-pending streaming update.

Mechanism: live-frame fast path bypasses the per-connection ordering queue

_cf_routeSubAgentConnectionOperation (packages/agents/src/index.ts:7246-7282) has two paths:

  1. Live frame: if the async-context bridge matches the connection id, the operation runs synchronously through the forwarded RPC bridge.
  2. No live frame: the operation is appended to _cf_subAgentConnectionOperationTails and only runs after await this._rootAlarmOwner() (an RPC round trip via getServerByName, packages/agents/src/index.ts:4056-4076) plus the root-routed send.

The fast path never consults the tail. Concrete sequence for one connection:

  • Delayed work (e.g. the streaming resume in the issue this PR fixes) calls connection.send(A) after its frame ended → queued; the queued task is still awaiting root resolution.
  • The browser sends another frame; the facet handles it and calls connection.send(B) inside that frame → matched active bridge → delivered immediately.
  • A is delivered afterwards, inverting call order — exactly the ordering guarantee the queue was added to provide (see also the new ordering test in packages/agents/src/tests/sub-agent-rpc-bridge.test.ts:246-262, which only covers the all-queued case).

The same inversion applies to setState (last write wins incorrectly) and close (a queued send can be dropped after an immediate close).

Prompt for agents
In Agent._cf_routeSubAgentConnectionOperation (packages/agents/src/index.ts around lines 7246-7282), the live-frame fast path executes the operation immediately whenever the async-context bridge matches the connection id, ignoring any operations still pending in _cf_subAgentConnectionOperationTails for that same connection. Because root-routed operations must first await _rootAlarmOwner() (an RPC round trip), an operation issued from a later client frame can be delivered before an earlier queued one, breaking the per-connection ordering this queue is meant to guarantee. Consider only taking the fast path when there is no pending tail for that connection id, or chaining the fast-path operation onto the existing tail (falling back to the root bridge if the frame bridge is no longer valid by the time the chain runs).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant