Skip to content

feat: per-connection protocol message control#920

Merged
threepointone merged 1 commit into
mainfrom
sp/per-connection-protocol-messages
Feb 16, 2026
Merged

feat: per-connection protocol message control#920
threepointone merged 1 commit into
mainfrom
sp/per-connection-protocol-messages

Conversation

@threepointone

Copy link
Copy Markdown
Contributor

Closes #881

Summary

  • Add shouldSendProtocolMessages(connection, ctx) — overridable hook on Agent (default: true). Called once during onConnect to decide whether protocol text frames should be sent to this connection.
  • Add isConnectionProtocolEnabled(connection) — predicate to check a connection's protocol status at any time, including after hibernation.
  • When shouldSendProtocolMessages returns false, the connection receives no CF_AGENT_IDENTITY, CF_AGENT_STATE, or CF_AGENT_MCP_SERVERS messages — neither on connect nor via subsequent broadcasts. RPC and regular messaging still work normally.
  • Also fixes a latent bug where isConnectionReadonly would return incorrect results after Durable Object hibernation.

Design decisions

Why a per-connection hook, not static options?

The original issue identified two needs: (1) per-connection control for DOs that serve mixed connection types (e.g. MQTT binary clients alongside web monitoring clients), and (2) agent-level feature opt-out. Per @threepointone's comment, option (2) is just the degenerate case of (1) — a hook that always returns false is effectively agent-level. So we only need the hook.

Why store the flag in connection state?

Same approach as _cf_readonly. The flag is persisted in the WebSocket attachment via connection.setState, which means it survives Durable Object hibernation. This is critical — @genmon reported that PR #892 leaked cf_agent_mcp_servers messages after hibernation wake because the in-memory state was lost.

How hibernation is handled

The core insight: both isConnectionReadonly and isConnectionProtocolEnabled call _ensureConnectionWrapped(connection) before reading the flag. This method is idempotent — if the _rawStateAccessors WeakMap already has the connection, it returns immediately. After hibernation, the WeakMap is empty, but the connection's state getter still reads from the persisted WebSocket attachment. _ensureConnectionWrapped re-captures that getter as getRaw, sets up the filtering overrides, and the predicate then reads the flag correctly. No separate fallback path needed.

This is cleaner than PRs #892/#893, which added a separate code path in the predicate that read connection.state directly when the WeakMap was empty — fragile because it depended on _ensureConnectionWrapped not having run yet.

Generalized internal key handling

Rather than hardcoding _cf_readonly in _ensureConnectionWrapped (as the current code does), the connection state wrapping now uses CF_INTERNAL_KEYS (a ReadonlySet) with module-level helpers (rawHasInternalKeys, stripInternalKeys, extractInternalFlags). Adding a future internal flag means adding one entry to the set — the wrapping logic handles it automatically.

_broadcastProtocol for filtered broadcasts

State broadcasts (_setStateInternal) and MCP broadcasts (broadcastMcpServers) now go through _broadcastProtocol, which builds an exclusion list of no-protocol connections and delegates to this.broadcast(). This keeps the filtering in one place.

Usage example

class MyAgent extends Agent {
  shouldSendProtocolMessages(connection, ctx) {
    // Suppress protocol messages for MQTT binary clients
    const url = new URL(ctx.request.url);
    return url.searchParams.get("protocol") !== "false";
    // Or check WebSocket subprotocol:
    // return ctx.request.headers.get("Sec-WebSocket-Protocol") !== "mqtt";
  }
}

Notes for reviewers

  1. isConnectionReadonly bug fix — The existing isConnectionReadonly had a latent hibernation bug: after DO hibernation, the _rawStateAccessors WeakMap is empty, so the method returned false for connections that were actually readonly. The shouldConnectionBeReadonly hook only runs during onConnect, which does NOT fire on hibernation wake. My fix (calling _ensureConnectionWrapped in the predicate) fixes this for both flags. This is a behavioral change to the readonly feature, but strictly a bug fix.

  2. No dynamic toggle — Unlike setConnectionReadonly(connection, bool) which can be toggled at runtime, _setConnectionNoProtocol is private and only called during onConnect. This is intentional per the issue thread: "We don't need it to be able to be dynamically set." Protocol status is evaluated once on connect and persisted.

  3. All protocol message sites covered — I audited every MessageType.CF_AGENT_* send site in index.ts. The three protocol types (IDENTITY, STATE, MCP_SERVERS) are filtered in onConnect (conditional block) and in broadcasts (_broadcastProtocol). CF_AGENT_STATE_ERROR is correctly NOT filtered — it's a direct reply to a specific connection that sent a bad state update, not an unsolicited broadcast.

  4. 16 tests covering: hook behavior (protocol sent / not sent on connect), predicate correctness, RPC on no-protocol connections, state broadcast filtering, mixed connections, reconnection re-evaluation, connection.state wrapping (flag hidden, preserved across setState value and callback forms), and readonly + no-protocol combination on the same connection.

Test plan

  • npm run build passes
  • npm run check passes (sherif, exports, oxfmt, oxlint, typecheck across 42 projects)
  • npm run test — all 529+ tests pass (including 16 new protocol-messages tests)
  • Existing readonly tests still pass (generalized wrapping is backward compatible)

Made with Cursor

Add `shouldSendProtocolMessages(connection, ctx)` hook and
`isConnectionProtocolEnabled(connection)` predicate for per-connection
control of protocol text frames (CF_AGENT_IDENTITY, CF_AGENT_STATE,
CF_AGENT_MCP_SERVERS).

This is useful for binary-only clients (e.g. MQTT devices) that cannot
handle JSON text frames. Connections with protocol messages disabled
still fully participate in RPC and regular messaging.

Also generalizes the internal connection state wrapping to support
multiple _cf_-prefixed flags, and fixes isConnectionReadonly to
correctly survive Durable Object hibernation.

Closes #881

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Feb 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e7c7241

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

This PR includes changesets to release 1 package
Name Type
agents Minor

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

@pkg-pr-new

pkg-pr-new Bot commented Feb 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@920

commit: e7c7241

@threepointone

Copy link
Copy Markdown
Contributor Author

/bonk review this pr

@ask-bonk

ask-bonk Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

I read all 7 changed files in the PR (the core index.ts diff, test agent, test file, changeset, worker registration, wrangler config, and agent index exports), then grep-searched for all references to the new APIs across the source tree to verify completeness of the implementation and audit all call sites, before writing the review summary.

github run

@threepointone
threepointone merged commit 4dea3bd into main Feb 16, 2026
4 checks passed
@threepointone
threepointone deleted the sp/per-connection-protocol-messages branch February 16, 2026 23:25
@github-actions github-actions Bot mentioned this pull request Feb 16, 2026
threepointone added a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 17, 2026
Add new page documenting `shouldSendProtocolMessages` and
`isConnectionProtocolEnabled` for per-connection control of protocol
text frames. Update cross-references in agents-api, websockets,
readonly-connections, store-and-sync-state, and mcp-client-api pages.

Corresponds to cloudflare/agents#920.

Co-authored-by: Cursor <cursoragent@cursor.com>
threepointone added a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 17, 2026
Add new page documenting `shouldSendProtocolMessages` and
`isConnectionProtocolEnabled` for per-connection control of protocol
text frames. Update cross-references in agents-api, websockets,
readonly-connections, store-and-sync-state, and mcp-client-api pages.

Corresponds to cloudflare/agents#920.

Co-authored-by: Cursor <cursoragent@cursor.com>
threepointone added a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 17, 2026
)

* Docs: add chat agents & retries; update APIs

Add comprehensive Chat agents and Retries reference pages. Update numerous API examples to use TypeScript "} satisfies ExportedHandler<Env>;" for exported handlers. Change Wrangler compatibility_date to a placeholder "$today". Add MCP client retry options and a custom OAuth provider example. Update queue API docs to mark several methods as synchronous and adjust signatures, and add a note about built-in queue retries. Miscellaneous related doc fixes and clarifications.

* Remove generic <Env> from Agent examples

Simplify Agent class examples in the agents docs by removing explicit generic type parameters (e.g. <Env>, <Env, never>) from class declarations. Updated multiple MDX files under src/content/docs/agents to match the current Agent API/signature and improve example readability. No runtime or behavioral changes — only documentation/type examples were adjusted.

* Standardize local secrets filename to .env

Replace references to `.dev.vars` with `.env` across Agents documentation and update corresponding command examples (touch/echo/cat) and troubleshooting notes. Files updated: src/content/docs/agents/api-reference/configuration.mdx, src/content/docs/agents/guides/remote-mcp-server.mdx, src/content/docs/agents/guides/slack-agent.mdx, and src/content/docs/agents/index.mdx to standardize local development secrets guidance.

* Clarify chat agent lifecycle hooks

Reword the AIChatAgent lifecycle section to clarify that overriding onConnect/onClose is for adding custom logic while stream resumption, message sync, and connection cleanup are handled automatically. Updated the example comments to remove the implication that calling super is required. Also reformatted the deprecated APIs table for consistent column alignment and readability.

* docs(agents): add protocol messages documentation

Add new page documenting `shouldSendProtocolMessages` and
`isConnectionProtocolEnabled` for per-connection control of protocol
text frames. Update cross-references in agents-api, websockets,
readonly-connections, store-and-sync-state, and mcp-client-api pages.

Corresponds to cloudflare/agents#920.

Co-authored-by: Cursor <cursoragent@cursor.com>

* PCX review

* Update src/content/docs/agents/api-reference/chat-agents.mdx

* Add intro sentence before table

Co-authored-by: threepointone <threepointone@users.noreply.github.com>

* docs(agents): add data parts section to chat agents reference

Document data-* stream parts: server-side writer.write() usage,
three patterns (reconciliation, append, transient), typed client
access via UIMessage generics, and onData callback for transient parts.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jun Lee <junlee@cloudflare.com>
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
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.

feature request: option to turn off agent protocol messages on the websocket

1 participant