feat: per-connection protocol message control#920
Merged
Conversation
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 detectedLatest commit: e7c7241 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
commit: |
Contributor
Author
|
/bonk review this pr |
Contributor
|
I read all 7 changed files in the PR (the core |
Merged
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>
This was referenced Feb 17, 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
) * 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #881
Summary
shouldSendProtocolMessages(connection, ctx)— overridable hook onAgent(default:true). Called once duringonConnectto decide whether protocol text frames should be sent to this connection.isConnectionProtocolEnabled(connection)— predicate to check a connection's protocol status at any time, including after hibernation.shouldSendProtocolMessagesreturnsfalse, the connection receives noCF_AGENT_IDENTITY,CF_AGENT_STATE, orCF_AGENT_MCP_SERVERSmessages — neither on connect nor via subsequent broadcasts. RPC and regular messaging still work normally.isConnectionReadonlywould 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
falseis 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 viaconnection.setState, which means it survives Durable Object hibernation. This is critical — @genmon reported that PR #892 leakedcf_agent_mcp_serversmessages after hibernation wake because the in-memory state was lost.How hibernation is handled
The core insight: both
isConnectionReadonlyandisConnectionProtocolEnabledcall_ensureConnectionWrapped(connection)before reading the flag. This method is idempotent — if the_rawStateAccessorsWeakMap already has the connection, it returns immediately. After hibernation, the WeakMap is empty, but the connection'sstategetter still reads from the persisted WebSocket attachment._ensureConnectionWrappedre-captures that getter asgetRaw, 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.statedirectly when the WeakMap was empty — fragile because it depended on_ensureConnectionWrappednot having run yet.Generalized internal key handling
Rather than hardcoding
_cf_readonlyin_ensureConnectionWrapped(as the current code does), the connection state wrapping now usesCF_INTERNAL_KEYS(aReadonlySet) 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._broadcastProtocolfor filtered broadcastsState broadcasts (
_setStateInternal) and MCP broadcasts (broadcastMcpServers) now go through_broadcastProtocol, which builds an exclusion list of no-protocol connections and delegates tothis.broadcast(). This keeps the filtering in one place.Usage example
Notes for reviewers
isConnectionReadonlybug fix — The existingisConnectionReadonlyhad a latent hibernation bug: after DO hibernation, the_rawStateAccessorsWeakMap is empty, so the method returnedfalsefor connections that were actually readonly. TheshouldConnectionBeReadonlyhook only runs duringonConnect, which does NOT fire on hibernation wake. My fix (calling_ensureConnectionWrappedin the predicate) fixes this for both flags. This is a behavioral change to the readonly feature, but strictly a bug fix.No dynamic toggle — Unlike
setConnectionReadonly(connection, bool)which can be toggled at runtime,_setConnectionNoProtocolis private and only called duringonConnect. 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.All protocol message sites covered — I audited every
MessageType.CF_AGENT_*send site inindex.ts. The three protocol types (IDENTITY,STATE,MCP_SERVERS) are filtered inonConnect(conditional block) and in broadcasts (_broadcastProtocol).CF_AGENT_STATE_ERRORis correctly NOT filtered — it's a direct reply to a specific connection that sent a bad state update, not an unsolicited broadcast.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.statewrapping (flag hidden, preserved acrosssetStatevalue and callback forms), and readonly + no-protocol combination on the same connection.Test plan
npm run buildpassesnpm run checkpasses (sherif, exports, oxfmt, oxlint, typecheck across 42 projects)npm run test— all 529+ tests pass (including 16 new protocol-messages tests)Made with Cursor