Repository navigation
Raise SessionLostError when the WebSocket drops mid-episode - #1536
Merged
sergiopaniego merged 2 commits intoOct 30, 2026
Merged
Conversation
Since openenv#1103 the client replaces a socket closed by the far end. The server keeps one environment per WebSocket, so the replacement is a new session that this client never reset, and a step() after it ran on another game with no error. The client now remembers whether reset() opened an episode on the current socket and raises SessionLostError from step()/state() if that socket is gone. reset() still reconnects as before. MCP clients in production mode reattach to the same HTTP MCP session, so they keep the episode and are left as is.
Clients whose tests replace _send_and_receive never open a socket, so a boolean flag made step() raise SessionLostError on them. The client now remembers the socket reset() used and raises only if that socket was closed or replaced.
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.
Summary
Since #1103,
EnvClientreplaces a WebSocket closed by the far end and reconnects on the next call. The server keeps one environment per WebSocket (websocket_endpointcalls_create_session()for each connection and destroys it on disconnect), so the new socket is a new session with an environment this client never reset. If the socket closes in the middle of an episode, the nextstep()runs on that fresh environment and returns observations from another game, with no error.The client now remembers the socket that
reset()used. If that socket was closed or replaced whenstep()orstate()is called, it raisesSessionLostError(aConnectionError), and keeps raising on everystep()orstate()untilreset(), which reconnects as before and never raises it. Otherwise a caller that retries the step (a trainer returning the error to the model as a tool result, for example) would land on the new session again.MCP clients in production mode reattach the new socket to the same HTTP MCP session (
MCPClientBase._connect_async), which outlives the WebSocket, so the episode carries over and they are left as is.How we hit it
GRPO with TRL
environment_factoryagainst a private Sudoku Space (textarena_env,MAX_CONCURRENT_ENVS=64), 64 clients that live for the whole run, onereset()and 15 to 25step()calls per game.The Space closed all 64 WebSockets about every 2 hours (server logs: 64
connection closed, then 64 newWebSocket /ws [accepted], with no error in the app). When a close landed while games were in progress, the model kept playing the board in its context while the server applied the moves to another board. That step: 0% solved and 7 moves per game, against 39% and 17 moves in the steps around it. Steps 74 and 75 were hit the same way later in the run. When a close landed between games nothing happened, because the reconnect happened onreset(). Nothing in the client or trainer logs showed the reconnects. We found them in the Space logs.Changes
src/openenv/core/env_client.py:SessionLostError, an_episode_wsreference to the socket the episode started on (set byreset(), cleared ondone, disconnect and close), and_raise_if_session_lost()called bystep()andstate()before sending.src/openenv/core/mcp_client.py: skip the check in production mode.src/openenv/core/__init__.py: exportSessionLostError.tests/envs/test_websockets.py: two tests next totest_protocol_client_reconnects_after_close.Type of Change
Alignment Checklist
Before submitting, verify:
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles.claude/docs/INVARIANTS.mdand no invariants are violated/pre-submit-pr(orbash .claude/hooks/lint.shand tests) and addressed all issuesRFC Status
Test Plan
test_protocol_client_raises_when_session_lost_mid_episodecloses the socket from under aGenericEnvClientin the middle of an episode, asserts the nextstep()raisesSessionLostError, and a retry too, then checks thatreset()and a step work on the new session. Onmainthe step goes through on a new session and the test fails withDID NOT RAISE.test_protocol_client_keeps_production_session_after_closechecks that anEchoEnv(production mode) keeps working after the same close.test_protocol_client_reconnects_after_closestill passes.The 3 failures (
test_protocol_client_state,test_protocol_client_multiple_episodes,test_fastapi_command_available) fail the same way onmainlocally.Note
Medium Risk
Changes runtime behavior when connections drop during an active episode: callers must handle SessionLostError and call reset(), but this prevents incorrect cross-session steps in long-running training clients.
Overview
Fixes silent mid-episode reconnects that could apply
step()/state()to a brand-new server environment after the WebSocket dropped, because each connection gets its own session untilreset().EnvClientnow tracks the socket used for the current episode (_episode_wsset afterreset(), cleared ondoneor disconnect). If that socket is gone beforestep()orstate(), it raisesSessionLostError(exported fromopenenv.core) instead of auto-reconnecting, and keeps raising on retries untilreset()starts a new episode. MCP clients in production mode skip this check because reconnect reuses the same HTTP MCP session.Adds websocket tests for the raise-on-lost behavior and for production MCP continuity after a close.
Reviewed by Cursor Bugbot for commit d27431a. Bugbot is set up for automated code reviews on this repo. Configure here.