Repository navigation
fix(agent-server): stop fanning streaming deltas out to every subscriber - #4689
Merged
Merged
Conversation
StreamingDeltaEvent is published straight to the conversation PubSub, which notified every subscriber with no kind filter. Consumers sized for conversation events therefore received traffic proportional to tokens: WebhookSubscriber POSTed each delta to the configured endpoint, and TelemetrySubscriber counted each one into event_count_bucket. Make delta delivery opt-in on the Subscriber base class and consult it in PubSub.__call__. The live WebSocket - the one consumer token streaming exists for - opts in; everything else, including subscribers added later, is excluded by default. Fixes #4672 Fixes #4673
Contributor
Python API breakage checks β β PASSEDResult: β PASSED |
Contributor
REST API breakage checks (OpenAPI) β β PASSEDResult: β PASSED |
Contributor
Cut the explanatory comments and test docstrings down to one line each, and only rebuild the subscriber list when the event is actually a delta so the common path keeps its original behaviour.
VascoSch92
marked this pull request as ready for review
August 27, 2026 17:56
enyst
approved these changes
Aug 27, 2026
enyst
left a comment
Member
There was a problem hiding this comment.
As noted on the fly in the first issue, I think itβs worth thinking about making deltas non-Events, that is, remove the inheritance. Iβm not sure off hand what the consequences are, itβs just, itβs been in the back of my mind since your HTML design doc.
Sorry, I know itβs not exactly in scope in this PR itself. Just me in a hurry. π
This was referenced Aug 28, 2026
Closed
1 of 5 tasks
1 of 5 tasks
1 of 5 tasks
5 tasks
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.
HUMAN:
Fixing a bug where we were publishing every delta instead of just the event.
AGENT:
Why
StreamingDeltaEventis published straight to the conversationPubSub(event_service.py:1061), andPubSub.__call__notified every registered subscriber with no kind filter. Making the delta anEventopted it into every consumer of the bus by default, so consumers sized for conversation events started receiving traffic proportional to tokens:WebhookSubscriber.__call__enqueues unconditionally, andevent_buffer_sizedefaults to 5, so a streamed response became a continuous POST loop to the configured endpoint for its whole duration. Model output is not masked on the standardAgentpath, so that text was leaving the process one fragment at a time.TelemetrySubscriber._handleincrements_event_countbefore itsmatch, soevent_count_bucketscaled with model verbosity instead of conversation activity, making streamed and non-streamed conversations incomparable.Same root cause, so one fix closes both.
Summary
Subscriber.receives_streaming_deltas(ClassVar, defaults toFalse) and consult it inPubSub.__call__: deltas are delivered only to subscribers that opt in._WebSocketSubscriberopts in β the live socket is the one consumer token streaming exists for.Delivery is opt-in rather than opt-out so a subscriber added later cannot inherit token-rate traffic β and the content it carries β by accident. This is option 2 from #4672 ("interim: a kind filter"); it does not preclude the epic's structural direction of taking deltas off the bus entirely.
Issue Number
Fixes #4672
Fixes #4673
How to Test
End-to-end β real HTTP egress. The script below runs a real HTTP receiver, a real
PubSuband a realWebhookSubscriber, then publishes one streamed response (3 conversation events + 200 token deltas) with the production defaultevent_buffer_size=5. It uses no new API, so it runs unchanged onmain:webhook_delta_e2e.py41 POSTs β 1, and that single POST is exactly what the equivalent non-streamed response sends β which is acceptance criterion 1 of #4672.Unit tests. Each fails on
mainand passes here:Regression sweep. Full agent-server suite plus pre-commit:
Video/Screenshots
Terminal output inline above β the before/after POST counts are the observable behaviour of this change.
Design Doc
n/a β the change is 11 added lines of source across two files.
Type
Notes
event_count_bucketwill step down for streamed conversations once this lands. That is the correction, but it is still a break in the series β worth a heads-up to whoever consumes the dashboard rather than a silent change.isinstanceguards. [Bug]: every token delta is POSTed to configured webhooksΒ #4672 offers either. Two guards fix today's two subscribers; the flag also protects the next one. There are currently five subscribers on this bus (_EventSubscriber,AutoTitleSubscriber,WebhookSubscriber,TelemetrySubscriber,_WebSocketSubscriber) and only the last wants deltas._EventSubscribercallsservice.touch()on every event, so it no longer sees deltas β butis_idle_evictable()already returnsFalsewhenever_run_taskis active, and a streamed response is by definition an active run. Dropping deltas there also removes astored.updated_atwrite per token.PubSubstays generic. Theisinstancecheck is a no-op forPubSub[BashEventBase]; the pre-commit import-dependency rule passes.π³ Agent Server images for this PR β GHCR package, pull/run commands, and all pushed tags (click to expand)
β’ GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:1f2aef9-pythonRun
All tags pushed for this build
About Multi-Architecture Support
1f2aef9-python) is a multi-arch manifest supporting both amd64 and arm641f2aef9-python-amd64) are also available if needed