Repository navigation
Conversation
Contributor
Author
|
/ok to test efac265 |
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
October 30, 2026 06:19
efac265 to
d565468
Compare
ffrujeri
changed the base branch from
ffrujeri/self-contained-environment-server
to
ffrujeri/self-contained-mcp
October 30, 2026 06:19
Contributor
Author
|
/ok to test d565468 |
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
October 30, 2026 16:21
d565468 to
dcd7c47
Compare
Contributor
Author
|
/ok to test dcd7c47 |
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
October 31, 2026 13:16
dcd7c47 to
34deb08
Compare
Contributor
Author
|
/ok to test 34deb08 |
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
November 2, 2026 23:01
0dc34e5 to
e9a058d
Compare
This was referenced Nov 2, 2026
Contributor
Author
|
/ok to test e9a058d |
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
November 4, 2026 21:33
e9a058d to
3893869
Compare
Contributor
Author
|
/ok to test 3893869 |
7 tasks done
… the worker that runs the episode With several uvicorn workers, a tool call for an episode can reach a worker that does not hold the episode, so BaseEnvironmentServerWithTools refused num_workers > 1. It now routes sessions to their owner, as Resources and Agent Servers do: - serve_tools() claims the episode, by its capture key, on the worker that runs it; the episode's cleanup releases the claim. A second serve of the same attempt on another worker is refused. - The router reads a tool call's episode from its episode token header, without the nonce, which only the episode's worker checks. The MCP grant carries the same header and /mcp is stateless, so every MCP request (initialize, tools/list, tools/call) is routed the same way. - session_routing gains a generic header source for the session ID (session_header with the server's reader) and SessionHolder, for a server that claims and releases each session itself. SessionKind's cookie key, body field and seed and close routes become optional. Resources and Agent Servers are unchanged. - An end-to-end test runs a test-only BaseEnvironmentServerWithTools with four workers, with episodes that call their tools over direct HTTP and over MCP; the routing e2e job also runs when the base class changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Felipe Vieira Frujeri <ffrujeri@nvidia.com>
ffrujeri
force-pushed
the
ffrujeri/self-contained-session-routing
branch
from
November 6, 2026 17:16
3893869 to
5daf3e5
Compare
Contributor
Author
|
/ok to test 5daf3e5 |
This branch was previously deployed
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.
Stacked on #5349 (MCP). The stack: #5346 (episode tokens) → #5347 (the base class) → #5348 (shutdown sweep) → #5349 (MCP) → this PR. Review those first; this PR's diff against #5349 is the routing.
What changed
With several uvicorn workers, a tool call for an episode can reach a worker that does not hold the episode.
BaseEnvironmentServerWithToolsnow routes each call to the worker that runs the episode, with the session routing that Resources and Agent Servers already use, and the interimnum_workers > 1refusal from #5347 is gone.serve_tools()claims the episode, by its capture key, on the worker that runs it. The episode's cleanup releases the claim when it stops serving the tools. A retried attempt (-a<N>) has its own key, so its own claim. Serving the same attempt on another worker is refused.X-NeMo-Gym-Episode-Token). It does not need the nonce; only the episode's worker checks it, so a forged token is forwarded and then refused there with 403.tools/list,tools/call) is routed by it./mcpis stateless Streamable HTTP, so no MCP session lives on a worker; each request only has to reach the episode's worker, which the e2e test checks.nemo_gym/server_runtime/request_handling/session_routing.py, generic and opt-in:session_headerandsession_from_header: a header that names the session, with the server's reader of its value, checked before the cookie and MCP token.SessionHolder: a server that claims and releases each session itself, as an Agent Server releases its own.SessionKind's cookie key, body field, and seed and close routes may beNone, for a kind named only in a header.SessionKind.path_claim, feat(agent): let a caller stop a running rollout and still grade it #5145) is unchanged. The episode kind sets none, so a tool call never takes that path.nemo_gym/server_runtime/base_environment_server.pychanges.Why
Linear GYMINT-645. Single-worker is not an option at Gym's scale, so an Environment Server that serves tools needs the same per-worker routing as a Resources Server. This removes the limit #5347 had to add.
Validation
pytest -n 12 tests/unit_tests/server_runtime/ tests/unit_tests/test_public_api.py tests/unit_tests/test_import_linter_coverage.py tests/unit_tests/test_moved_paths.py(after the rebase onto main atdda609f86)NEMO_GYM_MULTIWORKER_E2E=1 pytest tests/e2e/session_routing(after the rebase onto main atdda609f86)test_every_mcp_request_runs_on_the_worker_that_runs_its_episodeand main'stest_session_of_an_exited_worker_gets_an_explicit_error; both passed when rerunNEMO_GYM_MULTIWORKER_STRESS=1 NEMO_GYM_STRESS_EPISODES=400 NEMO_GYM_STRESS_SESSIONS=100 pytest tests/e2e/session_routing/test_session_routing_stress.py(after the rebase onto main atdda609f86)pytest tests/unit_tests/server_runtime/test_episode_tools_session_routing.pyserve_toolsdisabledNEMO_GYM_MULTIWORKER_E2E=1 pytest tests/e2e/session_routing/test_episode_tools_routing_e2e.pyBaseEnvironmentServerWithToolswith 4 real workers andtool_transports: [direct_http, mcp]: 200 episodes each make 8 direct HTTP tool calls through the public port on new connections; all 1,600 return 200 and ran on their episode's worker, and the episodes ran on several workers. 50 episodes each act as an MCP client (the MCP SDK's, without keep-alive, so each request lands on any worker): initialize,tools/listand 4tools/call; every episode listed exactly its tool and all 200 calls ran on its worker. Killing the worker that runs an episode makes its next tool call 410.tools/listrequests that reached other workers listed no toolsNEMO_GYM_MULTIWORKER_E2E=1 pytest tests/e2e/session_routingNEMO_GYM_MULTIWORKER_STRESS=1 NEMO_GYM_STRESS_EPISODES=400 NEMO_GYM_STRESS_SESSIONS=100 pytest tests/e2e/session_routing/test_session_routing_stress.py(before the rebase)pytest tests/unit_tests/server_runtime/test_base_environment_server_with_tools.py tests/unit_tests/server_runtime/test_episode_tools_session_routing.py tests/unit_tests/server_runtime/test_session_routing.py tests/unit_tests/server_runtime/test_episode_tools.py tests/unit_tests/server_runtime/test_episode_tokens.py tests/unit_tests/server_runtime/test_mcp_auto_exposure.py tests/unit_tests/server_runtime/test_checkpoint_resources_mcp.py tests/unit_tests/server_runtime/test_environment_server.py tests/unit_tests/server_runtime/test_base_server.py(after the rebase)pytest tests/unit_tests/server_runtime/test_base_resources_server.py tests/unit_tests/server_runtime/test_checkpoint_participant_workers.py tests/unit_tests/server_runtime/test_base_server.py tests/unit_tests/server_runtime/test_mcp_auto_exposure.py(before the rebase)pre-commit runon the changed filesRollout evidence
N/A: infrastructure for a base class that no environment uses yet. The e2e test runs real multi-worker server processes; BIRD-Interact's move onto the base class (Linear GYMINT-646) will run rollouts.
Compatibility
install_session_routingarguments are optional and they pass none.SessionKindfields are nowOptional. The two existing kinds set all of them.BaseEnvironmentServerWithToolsnow acceptsnum_workers > 1.🤖 Generated with Claude Code