Skip to content

feat(dev): replace raw Rsbuild HMR frames with a provider-owned Runtime App reload channel (#73) - #84

Merged
ScriptedAlchemy merged 2 commits into
mainfrom
cursor/runtime-app-reload-channel-a0dc
Aug 31, 2026
Merged

feat(dev): replace raw Rsbuild HMR frames with a provider-owned Runtime App reload channel (#73)#84
ScriptedAlchemy merged 2 commits into
mainfrom
cursor/runtime-app-reload-channel-a0dc

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Fixes #73

What this does

Runtime App refresh no longer depends on Rsbuild's private WebSocket protocol in any form. The reload signal now travels entirely over surfaces we own:

  1. Provider compile hook — the example's agent-bundle:rsc-runtime-app-reload plugin (formerly …app-hmr-token) invokes a plain onAppReload() callback for each later successful, changed App environment compilation. The dedup logic (first compile, errored compile, unchanged hash, dev-server lifecycle) is the previous hot.send('full-reload') gate unchanged; only the emission changed.
  2. Provider/session-owned channelRsbuildRuntimeSession relays the callback to subscribers of the trusted client-surface endpoint's new subscribeReload(listener) => unsubscribe field. The endpoint no longer carries webSocketOrigin/webSocketPath/webSocketToken; the Rsbuild HMR credential never leaves the compiler process.
  3. Proxy-owned browser channelRuntimeClientSurfaceProxy hosts its own one-way WebSocket at /__agent_bundle_runtime/reload (inside the namespace it already owns for bootstrap). It authors both ends: the server broadcasts {"kind":"runtime-app-reload","generation":N} frames, and the bootstrap shell it serves is the only intended client. It never dials Rsbuild's WebSocket.

Robustness gained over the previous relay

  • Upgrade immunity: a minor Rsbuild upgrade can no longer leave the socket connected while refresh silently stops — no Rsbuild frame is parsed anywhere.
  • Missed-reload catch-up: the proxy replays its current reload generation on every accepted connection, and the shell embeds the generation current at bootstrap-serve time. A reload that fires while the shell socket is down (or between bootstrap and first connect) is applied on reconnect instead of being lost.
  • Stale-refresh protection: the shell refreshes only when the generation strictly advances, so duplicate/replayed frames are idempotent. Existing reconnect backoff, pagehide teardown, payload bounds, and the abort-safe entry refresh are preserved unchanged.
  • Strictly one-way channel: any client that writes into the channel is released; upgrades with query strings, subprotocols, foreign origins, or missing capability cookies are rejected as before.

Relationship to #76 (not a revert)

#76 (24f0ea19) was the bounded conformance fix: it kept the raw Rsbuild socket but allowlisted only the provider-emitted full-reload kind, leaving private ok/hash/unknown frames inert. That removed the semantic error but still treated Rsbuild's undocumented envelope as an application protocol — the failure mode was silence, not misbehavior. This PR is the deeper replacement #76's comments pointed at: the envelope is no longer consumed at all, so the allowlist (and the token/path plumbing that fed the raw socket) is deleted rather than restored to its pre-#76 shape.

API changes (published agent-bundle package, minor bump via changeset)

  • DevRuntimeClientSurfaceEndpoint: +subscribeReload, -webSocketOrigin, -webSocketPath, -webSocketToken (trusted server-only type).
  • DevRuntimeClientSurfaceProxyBinding: -webSocketPath.
  • McpAppPreviewAppsSnapshot.clientSurface: -webSocketPath (was a hardcoded '/rsbuild-hmr' literal the Workbench only validated, never used); the Workbench client validation is updated in the same commit.
  • New export runtimeClientSurfaceReloadChannelPath for observability/tests.

Tests

  • Proxy unit tests now drive the owned protocol: generation advance/replay semantics, connect-time replay, one-way enforcement, upgrade hygiene, unsubscribe-on-close; Rsbuild-frame fixtures (ok/hash/full-reload) remain only to prove they are inert.
  • Example integration tests cover the onAppReload gate (first/duplicate/failed/non-App compiles emit nothing; later changed compiles emit once, across dev-server restarts).
  • HMR e2e: overview.e2e asserts a real source edit produces an owned advancing-generation frame and a single in-place child refresh; mcp-app-real.e2e covers connection loss/recovery plus injected upgrade-style frames — a legacy {type:'full-reload'} frame stays inert while an owned newer-generation frame reinstalls the App once; runtime-playground-hmr.e2e covers client connect/disconnect counters unchanged.

Validation (all on this branch)

  • pnpm build, pnpm typecheck (root + workbench + create-agent-bundle), example typecheck, pnpm lint: pass.
  • pnpm test:unit: pass.
  • Example integration: dev-provider.integration.test.ts, dev-invocation.integration.test.ts: pass.
  • Integration/e2e: dev-workbench.test.ts, mcp-app-frame, mcp-app-preview-browser, mcp-page-app-browser, runtime-playground-hmr.e2e, runtime-playground.e2e, overview.e2e, mcp-app-real.e2e: pass, except one pre-existing failure in dev-workbench.test.ts :: simulates and replays real epoch-bound hooks through the packaged foreground server, which fails identically on unmodified main (28830bd5) in this environment and is unrelated to this change (hook replay, not the relay).

Stayed in lane

No changes to #74/#75 territory (per-env output staging, cohort identity), no changes to the compile-observer FIFO pairing in rsbuild.config.ts (the reload plugin is a separate plugin in the same file), and no changes to rslib.ts, build.test.ts, hooks.test.ts, or packed-consumer suites. overview.e2e and mcp-app-real.e2e changed only where they previously asserted raw /rsbuild-hmr frames.

Open in Web Open in Cursor 

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8df8f6e

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

This PR includes changesets to release 1 package
Name Type
agent-bundle 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 Aug 31, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@84
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@84
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/rsc-runtime@84

commit: d95625f

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review August 31, 2026 17:52
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T17:55:56.325332Z d95625f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d95625f299

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-bundle/src/dev/runtime-client-surface-proxy.ts Outdated
Replace the raw Rsbuild HMR WebSocket relay with a provider-owned reload
channel. The client-surface endpoint now exposes subscribeReload, fed by
the App environment compile hook; the proxy hosts its own one-way reload
WebSocket, replays the current generation on reconnect, and refreshes the
opaque App child only when the generation strictly advances. Rsbuild's
private frames, dev.client.path, and webSocketToken leave the contract.

Fixes #73

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
A reload frame that arrived while the shell's refresh fetch was still in
flight advanced the seen generation without being applied, so the newer
compilation never installed and the server's replay-on-reconnect stayed
inert. Track the applied generation separately from the requested one:
advance it only when a refresh actually installs an entry, and run a
catch-up refresh when a newer generation was announced mid-flight. A
failed refresh now leaves its generation unapplied, so a replayed frame
retries instead of being swallowed.
@ScriptedAlchemy
ScriptedAlchemy merged commit aafcf83 into main Aug 31, 2026
7 of 8 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the cursor/runtime-app-reload-channel-a0dc branch August 31, 2026 21:40
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.

Replace raw Rsbuild HMR frames with a provider-owned Runtime App reload channel

2 participants