Skip to content

fix(sdk): reconnect remote conversation websocket#3987

Merged
neubig merged 1 commit into
OpenHands:mainfrom
bozhnyukAlex:fix/websocket-reconnect
Jul 9, 2026
Merged

fix(sdk): reconnect remote conversation websocket#3987
neubig merged 1 commit into
OpenHands:mainfrom
bozhnyukAlex:fix/websocket-reconnect

Conversation

@bozhnyukAlex

@bozhnyukAlex bozhnyukAlex commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

HUMAN:
This fixes a issue in RemoteConversation observed in production: after a transient events WebSocket disconnect, our SDK client stopped receiving live events because WebSocketCallbackClient exited permanently instead of reconnecting.


AGENT:

Why

WebSocketCallbackClient used to exit permanently after any ConnectionClosed. For transient WebSocket closes, a RemoteConversation could silently stop receiving live events and only catch up later through REST polling or manual reconciliation.

Summary

  • Retry transient WebSocket ConnectionClosed failures with the existing backoff flow instead of exiting the client loop.
  • Treat server-side fatal close codes 4001 (auth failure) and 4004 (conversation not found) as non-retryable.
  • Reconcile remote events after a replacement WebSocket subscription becomes ready, so events missed while disconnected are merged into the local cache.
  • Add regression coverage for retryable close, fatal close, and reconnect reconciliation callback behavior.

Issue Number

Fixes #3983

How to Test

Environment setup:

make build

Targeted WebSocket/reconciliation tests:

uv run pytest tests/sdk/conversation/remote/test_websocket_client.py tests/sdk/conversation/remote/test_websocket_subscription_ready.py

Broader remote conversation SDK tests:

uv run pytest tests/sdk/conversation/remote

Lint/type checks on changed files:

uv run pre-commit run --files openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py tests/sdk/conversation/remote/test_websocket_client.py

Repo lint target:

make lint

Live reconnect smoke test:

I also ran a local smoke script that starts a real websockets.serve(...) server, sends an initial state update, closes the first connection with code 1000, accepts the SDK client's reconnect, sends a second state update, and asserts:

  • the server saw two WebSocket connections;
  • the SDK callback received state-1 and state-2;
  • on_reconnect ran exactly once.

The script printed:

live websocket reconnect smoke passed

Video/Screenshots

N/A. This is SDK transport behavior covered by automated tests and the live reconnect smoke above.

Type

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

Notes

This PR does not change the agent-server WebSocket API. It uses the existing REST event reconciliation path after reconnect rather than resend_mode=all/since, avoiding duplicate delivery through user callbacks while still repairing the remote event cache.

No docs update: this is an internal SDK transport reliability fix with no user-facing API or configuration changes.

Co-authored-by: openhands <openhands@all-hands.dev>
@bozhnyukAlex bozhnyukAlex marked this pull request as ready for review July 4, 2026 07:35
@all-hands-bot all-hands-bot requested a review from neubig July 7, 2026 13:31
@all-hands-bot

Copy link
Copy Markdown
Collaborator

[Automatic Post]: I have assigned @neubig as a reviewer based on the repository MAINTAINERS file. Thanks in advance for the help!

This comment was posted by an AI agent (OpenHands) on behalf of the user.

@neubig

neubig commented Jul 9, 2026

Copy link
Copy Markdown
Member

Thanks a bunch, I took a look at the code and did some tests, it seems to be working properly.

@neubig neubig merged commit 5115af7 into OpenHands:main Jul 9, 2026
43 checks passed
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.

[Bug]: WebSocketCallbackClient exits permanently after transient ConnectionClosed

3 participants