Found while diagnosing #3728. The WebSocket events transport silently fell back to HTTP for the entire life of every affected process, from #3493 (2026-08-19) until it was noticed by hand in durabench. Every CI lane stayed green throughout.
Three gaps, each independently sufficient to hide it:
1. e2e-vercel-ws-transport does not assert the transport
The lane exercises a run and checks the run's outcome. Events written over HTTP produce the same outcome as events written over WS, so the lane passes either way — it asserts that the feature is harmless, not that it is used.
Fix: assert which transport actually carried the events. postEventFrameOverWs already tags its span workflow.events.transport: 'ws' (see ws-transport-spans.test.ts), so there is something concrete to assert on.
2. No workbench warms the world in instrumentation.ts
workbench/nextjs-turbopack/instrumentation.ts only calls registerOTel. The failing shape needs register() to touch the world:
const { getWorld } = await import('workflow/runtime');
await getWorld();
That is what durabench's apps/workflow-host does, and it is why durabench saw this and CI did not. Worth adding to one workbench app so the deployed lane has the shape.
Note this is not required to duplicate the module — with no warm-up at all there are still two live copies (route + ssr) — but it is what makes the two World caches land in different copies deterministically.
3. resolveWsTransport misses silently
A miss returns null and the caller writes over HTTP with no breadcrumb. A single debug log naming the wsUrl and the registry size would have turned several investigations into one grep. Cheap, and useful well beyond this bug.
Why it matters beyond the one regression
#3728 adds a lint rule that prevents the cause (mutable module-scope state in a bundled world) from recurring. These three items are about the symptom being observable: the transport can also break for reasons that have nothing to do with module duplication — a proxy, an auth failure, a handshake regression — and today all of those are equally invisible.
Found while diagnosing #3728. The WebSocket events transport silently fell back to HTTP for the entire life of every affected process, from #3493 (2026-08-19) until it was noticed by hand in durabench. Every CI lane stayed green throughout.
Three gaps, each independently sufficient to hide it:
1.
e2e-vercel-ws-transportdoes not assert the transportThe lane exercises a run and checks the run's outcome. Events written over HTTP produce the same outcome as events written over WS, so the lane passes either way — it asserts that the feature is harmless, not that it is used.
Fix: assert which transport actually carried the events.
postEventFrameOverWsalready tags its spanworkflow.events.transport: 'ws'(seews-transport-spans.test.ts), so there is something concrete to assert on.2. No workbench warms the world in
instrumentation.tsworkbench/nextjs-turbopack/instrumentation.tsonly callsregisterOTel. The failing shape needsregister()to touch the world:That is what durabench's
apps/workflow-hostdoes, and it is why durabench saw this and CI did not. Worth adding to one workbench app so the deployed lane has the shape.Note this is not required to duplicate the module — with no warm-up at all there are still two live copies (route +
ssr) — but it is what makes the two World caches land in different copies deterministically.3.
resolveWsTransportmisses silentlyA miss returns
nulland the caller writes over HTTP with no breadcrumb. A single debug log naming thewsUrland the registry size would have turned several investigations into one grep. Cheap, and useful well beyond this bug.Why it matters beyond the one regression
#3728 adds a lint rule that prevents the cause (mutable module-scope state in a bundled world) from recurring. These three items are about the symptom being observable: the transport can also break for reasons that have nothing to do with module duplication — a proxy, an auth failure, a handshake regression — and today all of those are equally invisible.