Skip to content

Commit 858cbfe

Browse files
Fix RemoteEventsList to filter out full-state snapshot events
The RemoteEventsList default callback was including FULL_STATE_KEY ConversationStateUpdateEvents delivered over WebSocket that are NOT stored in the server-side EventLog. This caused the client-side event count to diverge from the server, breaking fork event-count parity in RemoteConversation.fork(). Co-authored-by: openhands <openhands@all-hands.dev>
1 parent e73d404 commit 858cbfe

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,22 @@ def append(self, event: Event) -> None:
375375
self.add_event(event)
376376

377377
def create_default_callback(self) -> ConversationCallbackType:
378-
"""Create a default callback that adds events to this list."""
378+
"""Create a default callback that adds conversation events to this list.
379+
380+
Filters out full-state snapshot events
381+
(``ConversationStateUpdateEvent`` with ``key == FULL_STATE_KEY``)
382+
that are delivered over the WebSocket but are **not** stored in the
383+
server-side ``EventLog``. Including them would cause the
384+
client-side event count to diverge from the server-side count,
385+
breaking consistency guarantees (e.g. fork event-count parity).
386+
"""
379387

380388
def callback(event: Event) -> None:
389+
if (
390+
isinstance(event, ConversationStateUpdateEvent)
391+
and event.key == FULL_STATE_KEY
392+
):
393+
return
381394
self.add_event(event)
382395

383396
return callback

tests/sdk/conversation/remote/test_remote_events_list.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,60 @@ def test_remote_events_list_callback_integration(mock_client, conversation_id):
149149
assert events_list[0].id == "callback-event"
150150

151151

152+
def test_default_callback_filters_full_state_snapshots(mock_client, conversation_id):
153+
"""Default callback must ignore full-state snapshot events.
154+
155+
Full-state snapshots (``ConversationStateUpdateEvent`` with
156+
``key == FULL_STATE_KEY``) are published via ``_pub_sub`` when a
157+
WebSocket subscriber connects or a run completes, but they are
158+
**not** stored in the server-side ``EventLog``. Including them
159+
would cause the client-side event count to diverge from the server.
160+
161+
Per-field state updates and other event types must still pass through.
162+
"""
163+
from openhands.sdk.event.conversation_state import (
164+
FULL_STATE_KEY,
165+
ConversationStateUpdateEvent,
166+
)
167+
from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
168+
169+
mock_response = create_mock_api_response([])
170+
mock_client.request.return_value = mock_response
171+
172+
events_list = RemoteEventsList(mock_client, conversation_id)
173+
callback = events_list.create_default_callback()
174+
175+
# Full-state snapshot should be filtered
176+
full_snapshot = ConversationStateUpdateEvent(
177+
key=FULL_STATE_KEY,
178+
value={"execution_status": "finished"},
179+
)
180+
callback(full_snapshot)
181+
assert len(events_list) == 0
182+
183+
# Per-field state update (key != FULL_STATE_KEY) should pass through
184+
field_update = ConversationStateUpdateEvent(
185+
key="execution_status",
186+
value="finished",
187+
)
188+
callback(field_update)
189+
assert len(events_list) == 1
190+
191+
# LLMCompletionLogEvent should also pass through (persisted in EventLog)
192+
llm_log_event = LLMCompletionLogEvent(
193+
filename="test.json",
194+
log_data="{}",
195+
)
196+
callback(llm_log_event)
197+
assert len(events_list) == 2
198+
199+
# Regular conversation events should pass through
200+
normal_event = create_mock_event("regular-event")
201+
callback(normal_event)
202+
assert len(events_list) == 3
203+
assert events_list[2].id == "regular-event"
204+
205+
152206
def test_remote_events_list_api_error(mock_client, conversation_id):
153207
"""Test error propagation when API calls fail."""
154208
mock_request = Mock()

0 commit comments

Comments
 (0)