anomalyco / anomalyco/opencode
Web UI: a tab's /global/event stream silently stops delivering when several tabs are connected
@Brendonovich is already working on this.
Since Jul 30, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
With several web UI tabs connected to one server, a tab's /global/event SSE stream can stop delivering entirely while other tabs keep working. The connection stays open — no error, no close — so the client sees a live socket that never produces another byte. The session keeps running server-side and the tab freezes mid-message until it is reloaded.
Steps to reproduce
opencode web --port 4096, open one browser tab, chat normally — stable, no interruption.- Add a second tab, chat in both — still stable.
- Add several more tabs (6 total in my case).
- Chat in the first and second tabs — still fine.
- Send a message in a third tab, then watch the first tab.
The first tab's stream goes silent. It is load-dependent rather than time-dependent: it correlates with event volume across subscribers, not with how long a connection has been open.
At the same moment the server logs:
MaxListenersExceededWarning: Possible EventTarget memory leak detected.
11 event listeners added to [yZ]. MaxListeners is undefined.
at addListener (node:events:158:22)
at ~effect/Effect/evaluate
_eventsCount: 1 and type: "event" identify it as GlobalBus, which is a plain Node EventEmitter — one listener per SSE subscriber, warning past Node's default of 10.
Suspected cause
The v1 subscriber attaches to GlobalBus and offers into a stream queue with no sizing (packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts):
const events = Stream.callback<GlobalBusEvent>((queue) => {
const handler = (event: GlobalBusEvent) => Queue.offerUnsafe(queue, event)
return Effect.acquireRelease(
Effect.sync(() => GlobalBus.on("event", handler)),
() => Effect.sync(() => GlobalBus.off("event", handler)),
)
})
Stream.callback accepts bufferSize and strategy; neither is passed, so each subscriber gets the default buffer. Queue.offerUnsafe is non-blocking, so once a subscriber's buffer fills its events are dropped rather than queued or back-pressured — and if that buffer never drains, the subscriber goes permanently silent while the connection stays open.
The v2 handler sizes this explicitly (packages/server/src/handlers/event.ts):
const subscriberCapacity = 256
const live = yield* EventV2.allBounded(events, subscriberCapacity)
so the v1 path looks like it simply never received the same treatment.
The listener count reaching 11 with 6 tabs also suggests subscribers are not always released promptly on disconnect, which would compound this over time.
I have not confirmed Effect's default bufferSize, so the exact threshold is unverified — but the v1/v2 asymmetry and the reproduction above both point here.
Impact
Any multi-tab web UI user. The session keeps running and its output is retained server-side, so nothing is lost, but the tab appears frozen mid-answer until reloaded.
Client-side recovery is being added separately in #39349 (a stall watchdog plus state repair on reconnect), which makes a stalled tab recover on its own within ~45s. That mitigates the symptom; this issue is the underlying cause.
Environment
- opencode 1.18.5,
opencode web, Firefox - v1 protocol (
/global/event)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.