Skip to content

fix(agent-server): keep streaming deltas feeding the activity heartbeat - #4712

Draft
all-hands-bot wants to merge 1 commit into
mainfrom
fix/4689-streaming-delta-idle-clock
Draft

all-hands-bot wants to merge 1 commit into
mainfrom
fix/4689-streaming-delta-idle-clock

Conversation

@all-hands-bot

@all-hands-bot all-hands-bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

HUMAN:


AGENT:

Why

PR #4689 ("stop fanning streaming deltas out to every subscriber") made
StreamingDeltaEvent delivery opt-in per subscriber, defaulting to off. The
internal _EventSubscriber (in conversation_service.py) is the conversation's
activity heartbeat — its __call__ refreshes both the idle-eviction clock
(EventService.touch()) and the runtime-api idle timer
(server_details_router.update_last_execution_time()). It was not opted in, so
streaming deltas silently stopped reaching it.

Impact (lens #9 semantic assertions / #6 background work): a long-running
streaming generation (stream=True) no longer refreshes the runtime idle clock.
After the ~20-minute idle timeout the runtime-api kills the pod mid-generation.
This is the exact failure the ACP activity heartbeat was added to prevent (see
_setup_acp_activity_heartbeat); regular SDK LLM agents relied on
_EventSubscriber receiving the token stream and now regress.

Root cause: the opt-in filter in PubSub.__call__ drops every non-websocket
subscriber for StreamingDeltaEvent, including the activity heartbeat, which
was never marked as a delta consumer.

Summary

Issue Number

N/A

How to Test

Source PR reviewed: #4689. Risk lenses: #9 (semantic assertions) and #6
(background work / idle-eviction clocks).

Deterministic reproduction — a StreamingDeltaEvent published through the
conversation's PubSub must advance both idle clocks:

uv run pytest tests/agent_server/test_conversation_eviction.py::test_streaming_delta_refreshes_idle_clock -x -q
  • On unmodified main, this test fails: idle_seconds() keeps advancing and
    time.time() - server_details_router._last_event_time stays at 10000+ seconds
    because _EventSubscriber.__call__ never runs for a delta.
  • With the fix, it passes (both clocks reset).

Before/after test results:

Before (main, without fix):

FAILED tests/agent_server/test_conversation_eviction.py::test_streaming_delta_refreshes_idle_clock
1 failed, 6 warnings in 1.14s

After (with fix):

1 passed, 6 warnings in 0.47s

Full affected suites still green:

uv run pytest tests/agent_server/test_conversation_eviction.py -q          # 9 passed
uv run pytest tests/agent_server/test_event_streaming.py \
             tests/agent_server/test_webhook_subscriber.py \
             tests/agent_server/telemetry/test_telemetry_subscriber.py -q  # 96 passed

End-to-end validation: the regression test exercises the real
ConversationService.start_conversation → EventService._pub_sub →
_EventSubscriber.__call__ path (no mocks), publishing a real
StreamingDeltaEvent and asserting the observable invariant — the runtime idle
clock is reset — rather than just "the event was emitted".

Why the original tests did not catch this: #4689's tests assert only that
deltas are filtered out of non-opted-in subscribers (webhook/telemetry) and
delivered to opted-in websockets. They never assert the activity-heartbeat
side effect of the internal subscriber, which is the mechanism that keeps the
pod alive during long streaming runs.

Video/Screenshots

N/A (server-side behavior; command output captured above).

Design Doc

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

This PR was created by an AI agent (OpenHands) on behalf of the weekly
high-impact regression hunt. Source PR #4689 was authored by @VascoSch92 and
approved by @enyst; both are requested as reviewers.


🐳 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

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:685a469-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-685a469-python \
  ghcr.io/openhands/agent-server:685a469-python

All tags pushed for this build

ghcr.io/openhands/agent-server:685a469-golang-amd64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-golang-amd64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-golang-amd64
ghcr.io/openhands/agent-server:685a469-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:685a469-golang-arm64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-golang-arm64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-golang-arm64
ghcr.io/openhands/agent-server:685a469-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:685a469-java-amd64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-java-amd64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-java-amd64
ghcr.io/openhands/agent-server:685a469-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:685a469-java-arm64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-java-arm64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-java-arm64
ghcr.io/openhands/agent-server:685a469-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:685a469-python-amd64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-python-amd64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-python-amd64
ghcr.io/openhands/agent-server:685a469-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:685a469-python-arm64
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-python-arm64
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-python-arm64
ghcr.io/openhands/agent-server:685a469-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:685a469-golang
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-golang
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-golang
ghcr.io/openhands/agent-server:685a469-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:685a469-java
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-java
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-java
ghcr.io/openhands/agent-server:685a469-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:685a469-python
ghcr.io/openhands/agent-server:685a4691b7445ccdc185868a7f7ed40ea2f0c55a-python
ghcr.io/openhands/agent-server:fix-4689-streaming-delta-idle-clock-python
ghcr.io/openhands/agent-server:685a469-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 685a469-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 685a469-python-amd64) are also available if needed

PR #4689 made streaming deltas opt-in for subscribers, but the internal
_EventSubscriber was not opted in. That subscriber doubles as the activity
heartbeat: it refreshes both the idle-eviction clock and the runtime-api
idle timer (update_last_execution_time). Detaching it from the token
stream means a long-running streaming generation never refreshes the
runtime idle clock, so the runtime-api can kill the pod mid-generation
(~20 min idle timeout).

Opt _EventSubscriber into streaming deltas so it keeps refreshing
activity, while webhooks/telemetry still skip deltas.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_service.py120012789%182–183, 192, 219–220, 224–225, 230, 378–379, 382–383, 399–400, 414, 587–588, 649, 722, 744, 751–752, 835, 911, 962–963, 970, 1002–1003, 1019, 1050, 1054, 1071, 1083–1086, 1092–1093, 1102, 1104, 1170, 1180, 1205, 1211–1212, 1216–1217, 1225, 1252, 1258, 1352, 1358, 1363, 1369, 1377–1378, 1387–1390, 1399, 1411, 1419, 1448, 1454–1455, 1458–1460, 1487, 1539, 1641–1642, 1713, 1768–1770, 1772–1773, 1776–1777, 1814, 1888–1889, 1920–1922, 1925–1926, 1930–1932, 1935–1936, 1940–1942, 1945–1946, 1975, 1984, 2027, 2037–2039, 2099, 2102, 2129, 2139, 2144–2147, 2161, 2172, 2184–2185, 2217, 2312, 2369, 2427, 2442–2443, 2828, 2881, 2884
TOTAL415271726958% 

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.

2 participants