Repository navigation
fix(resources): tighten the session contract for close, refusals and lost sessions - #4679
Merged
yaoyu-33 merged 10 commits intoOct 24, 2026
Merged
Conversation
yaoyu-33
marked this pull request as ready for review
October 16, 2026 17:23
…ess close Add ResourcesSessionFence, which serializes seed and close per resources_session_id and remembers closed sessions for CLOSED_SESSION_RETENTION_S instead of until shutdown. A seed for a closed session gets 409, which single_agent_turn does not retry. State that close must release state by resources_session_id when the request carries no cookie, as after a lost seed reply, and check it in check_resources_session_contract with an optional holds_state probe. Move example_single_tool_call onto the fence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…s at verify Seed and close refusals that no retry can fix were plain ValueErrors, so they became a 500 and single_agent_turn dispatched the whole episode again. Add ResourcesSeedRejectedError (422) for input that fails validation and ResourcesSessionConflictError (409) for an ID bound to another episode, a close for another episode, or a closed session. A verify for a session the server no longer holds returned reward 0 (a policy failure) or an error. Add lost_session_verification, which builds a masked sample with failure_kind session_lost or session_expired, and ResourcesSessionLostError (404) for tool calls on such a session. check_resources_session_contract now requires 409 for a conflicting or late seed and, with verify_body, a masked verify after close. Move gdpval and nemo_user_sim, which already run the check, onto these rules; nemo_user_sim also maps resources_session_id to its cookie session so a cookie-less close releases it and a conflicting seed is caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
yaoyu-33
force-pushed
the
fix/resources-session-close-contract
branch
from
October 19, 2026 23:35
1275512 to
9fab94c
Compare
yaoyu-33
added a commit
to yaoyu-33/Gym
that referenced
this pull request
Oct 19, 2026
…rn failures Resources servers now refuse unfixable seeds and closes with 422 or 409 and a reason in the body (NVIDIA-NeMo#4679), but single_agent_turn built failure_reason from str(ClientResponseError) for every stage except the agent seed, so the reason was lost. Use describe_dependency_error for the resources seed, agent turn, agent close and verify failures too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…completion A verify for a session gdpval no longer holds now returns a masked session_lost sample instead of a 409. The multistage orchestrator decides success by the failure class alone, so it counted that sample as a judged success: a final stage with half its tasks judged reported complete with a headline ELO, and a partial earlier stage was coverage-blocked and never retried. tag_results now turns a session_lost or session_expired result into the terminal verification failure the 409 used to produce, so routing, success, resume gating and partial-stage acceptance behave as before. aggregate_metrics also leaves such a row out of its stage's observed rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…view - gdpval: bind the session cookie only after a seed's checks pass, so a refused 409 or 422 reply no longer hands the caller another episode's session. - example_single_tool_call and nemo_user_sim: a close of a closed session that names another episode gets 409, and check_resources_session_contract now checks it (keeps_state only). nemo_user_sim records the closing episode. - nemo_user_sim: a seed dropped after session_ttl_seconds verifies as a masked session_expired sample instead of session_lost. - lost_session_verification logs a warning, since a masked sample is otherwise visible only in coverage metrics, and documents the dict merge that avoids a duplicate-keyword TypeError with extra="allow" requests. - ResourcesSessionLostError says what each harness does with the 404 and validates its failure_kind. - Tests: fence locks are per session, clear() forgets records, the contract check rejects a 500 late seed, refusals reach the wire as 422/404, a masked sample is kept for coverage and dropped from quality, gdpval TaskData 422 and judged-not-masked gone session, nemo_user_sim close by ID first. - Docs: the resources server tutorial and the nemo_user_sim README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Contributor
Author
|
/ok to test 6a1ac48 |
Contributor
|
🌿 Preview your docs: https://nvidia-preview-pr-4679-7ebecb5f759d.docs.buildwithfern.com/nemo/gym |
main added ClosedResourcesSessionRecords and session routing (NVIDIA-NeMo#3905): closed resources sessions are remembered for the configurable session_close_retry_window_seconds, a retried close keeps the first record, and with several workers a record must not outlive the session's routing claim. ResourcesSessionFence now keeps its per-session lock and delegates closed records to ClosedResourcesSessionRecords[EpisodeId], constructed with the server's session_close_retry_window_seconds. CLOSED_SESSION_RETENTION_S (a fixed 3600 s that renewed on every close) is gone. example_single_tool_call and gdpval build the fence from their config. The close-contract docstring now covers both a routed server, where a typed close is given resources_session_id even without a cookie, and an app without routing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
yaoyu-33
added a commit
to yaoyu-33/Gym
that referenced
this pull request
Oct 21, 2026
…-fence-migration Brings in NVIDIA-NeMo#4679's merge of main, where ResourcesSessionFence keeps closed records in ClosedResourcesSessionRecords for the server's session_close_retry_window_seconds. swebench_pro and interactive_browser now build their fence from that config, and the interactive_browser README states the configurable window instead of CLOSED_SESSION_RETENTION_S. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Contributor
Author
|
/ok to test ff3ecc4 |
yaoyu-33
disabled auto-merge
October 24, 2026 03:03
yaoyu-33
enabled auto-merge
October 24, 2026 03:03
weijiac0619
approved these changes
Oct 24, 2026
Contributor
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40110947808 |
kbhardwaj-nvidia
added a commit
that referenced
this pull request
Oct 24, 2026
#4679 made check_resources_session_contract require typed refusals and masked verifies for lost sessions, which failed this server's contract test in the merge queue. - Serialize seed and close with ResourcesSessionFence, which also bounds how long a closed session is remembered. - Refuse an unrunnable task with ResourcesSeedRejectedError (422) and a conflicting seed or close with ResourcesSessionConflictError (409); a close for another episode was a 500 that re-dispatched the episode. - Set the session cookie only after a seed succeeds. - Answer a tool call for a session the server no longer holds with ResourcesSessionLostError (404), and its verify with a masked session_lost sample instead of a 409. Signed-off-by: Khushi Bhardwaj <kbhardwaj@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
linj-glitch
pushed a commit
to linj-glitch/Gym
that referenced
this pull request
Oct 25, 2026
…irrup (NVIDIA-NeMo#5000) * feat(enterpriseops_gym): run EnterpriseOps-Gym on OpenSandbox with Stirrup Add ServiceNow's EnterpriseOps-Gym as a benchmark, run the way Artificial Analysis runs EnterpriseOps-Gym-AA: the Stirrup harness with only each task's oracle tools, a 100-turn cap, 3 repeats and strict pass@1. - resources_servers/enterpriseops_gym: for each task, start its gym servers from the upstream images in sandboxes (OpenSandbox), seed the task databases from the pinned upstream dumps, forward the agent's tool calls to the gym that owns each tool, and score the final database state with the task's SQL verifiers. Verifier semantics are checked against goldens produced by the upstream harness. - benchmarks/enterpriseops_gym: prepare the 649 public oracle tasks from the pinned Hugging Face dataset and pair the server with the Stirrup agent through an environment-server taskset. - responses_api_agents/stirrup_agent: add `code_exec` (default true) to leave out the shell tool when the task's tools are the only tools. Signed-off-by: Khushi Bhardwaj <kbhardwaj@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(enterpriseops_gym): follow the tightened resources-session contract NVIDIA-NeMo#4679 made check_resources_session_contract require typed refusals and masked verifies for lost sessions, which failed this server's contract test in the merge queue. - Serialize seed and close with ResourcesSessionFence, which also bounds how long a closed session is remembered. - Refuse an unrunnable task with ResourcesSeedRejectedError (422) and a conflicting seed or close with ResourcesSessionConflictError (409); a close for another episode was a 500 that re-dispatched the episode. - Set the session cookie only after a seed succeeds. - Answer a tool call for a session the server no longer holds with ResourcesSessionLostError (404), and its verify with a masked session_lost sample instead of a 409. Signed-off-by: Khushi Bhardwaj <kbhardwaj@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: Khushi Bhardwaj <kbhardwaj@nvidia.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
yaoyu-33
added a commit
that referenced
this pull request
Oct 27, 2026
…1-finance-sec-search-sessions #4679 is now on main (7dad3a5), so its pre-merge commits on this branch resolve to main's copy. The one conflict, resources_servers/nemo_user_sim/README.md, takes main's wording (#3910 reworded #4679's text). After the merge the branch differs from main only in resources_servers/finance_sec_search/. Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
copy-pr-bot Bot
pushed a commit
that referenced
this pull request
Oct 28, 2026
Main's session contract (#4679) refuses, with 409, a close that names another episode after the session closed, and the mock-tool server already records the closing episode. The SWE server recorded only that a session closed, so such a close got 200. It now records the episode that closed the session (the seed's episode when a retired attempt cleared it) and refuses a close for any other. A verify that arrives after the close is masked as `session_lost`, since the session's sandbox, and with it the patch, is gone. The contract test now also checks that a close without the cookie releases the sandbox and that the late verify is masked, and two tests mirror the mock-tool server's: a closed or retired session keeps its episode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com>
anson627
pushed a commit
to anson627/Gym
that referenced
this pull request
Oct 28, 2026
… checks (NVIDIA-NeMo#5007) * feat(partner_if): add the three SWE tasks to the partner_if benchmark partner_if grows to 25 tasks (dataset 2.1.0). pif_024-026 run OpenCode (opencode_agent) in the row's Scale-SWE sandbox, and swe_app.py grades their instruction following. - swe_app.py serves an Environment Server session for opencode_agent. A typed seed starts the row's image, runs the row's pre_commands and Scale-SWE's setup, and hands the sandbox to the agent as sandbox_access; a failed step stops the sandbox and fails the seed. Close, retire and shutdown stop it; per-session locks; one worker. Grading reads the actor's system prompt and tool-description additions from the row's contract (new GradingContract fields) and refuses a contract without the prompt. grade_if is split from Scale-SWE's native verify, which stays a diagnostic, and VERIFIER_FIXTURE covers full, zero and malformed cases. - benchmarks/partner_if adds the SWE environment and routes. The policy's sampling (temperature 1.0, top_p 1.0, no output cap) is pinned on a copy of the policy model, since opencode_agent refuses request-level sampling. The SWE agent inherits only opencode_agent's command deny-list: OpenCode 1.17.11 lets a tools map undo every permission denial. The SWE sandbox keeps the cluster's S3 mount out. prepare.py pins 2.1.0 (25 rows). - configs/swe.yaml follows the same wiring; the READMEs and manifest describe the SWE tasks. Validation: gym env test --resources-server partner_if (477 passed); the prepare and dispatcher tests (26 passed); gym env validate partner_if --kind benchmark (25 rows); pre-commit; a smoke on OpenSandbox with GLM 5.3 (pif_001 and pif_024-026, one rollout each: 4/4 healthy, 0 masked). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): keep judge calls out of rollout capture and carry tool records Make partner_if pass Frontier Eval's EC-2 check (gym-p0/v1) without changing Gym core. - TE-8/TE-9: the grading service calls the judge outside the rollout context. With observability on, ServerClient prefixed judge calls made during /verify with the rollout ID, so the model server captured them under the rollout with no owning invocation (model_call_ownership_unavailable). Outside the rollout context the judge call is forwarded without capture; the row keeps the judge request and reply as receipts. NVIDIA-NeMo#4630 adds verifier ownership in core; this can go once it lands. - TE-5: the dispatcher adds the tool executions in simple_agent's reply trajectory to the episode's agent observations. In an agent session simple_agent returns only its invocations at close, so the canonical ng_trajectory had no tool records. This is the conversion nemo_user_sim already uses; rollout collection joins each record's output from the invocation's conversation. Validation: partner_if tests 467 passed, dispatcher tests 20 passed, pre-commit clean. A full DFW run (66 rollouts, GLM 5.3 policy and judge, model-call capture on) passes EC-2: text rows with --no-tools and tool rows with the default scope both exit 0, TE-1 to TE-9 on every rollout; 279/279 tool calls have canonical records; health 66/66; pass@1[avg-of-3]/all_if_pass 63.64. Signed-off-by: Katherine Huang <katherineh@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(partner_if): say where the judge's calls are recorded Since the judge call runs outside the rollout context, it no longer appears in the model-call capture. The README now says that each row keeps the judge's request and reply in judge_request and judge_response, so the exit check on the judge's calls reads them there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): fit SWE evidence in the judge's input and retry an invalid judge reply In the 25-task exit checks, 15 of 27 SWE rollouts went unscored: 10 judge inputs exceeded the 1,000,000-character limit, and 5 judge replies were invalid, all on inputs above 800,000 characters. - SWE evidence: a tool output longer than 500 characters keeps its first and last 250, with a `[… N characters omitted …]` marker between them. Every agent message and tool call stays whole, in order. The answer is the agent's final message instead of every message joined, which the trajectory already holds. None of the SWE rubrics grades tool output. On the 27 exit-check rollouts the largest judge input goes from 1,464,448 to 841,649 characters, and the formerly invalid ones from 0.81-1.00M to 0.47-0.59M. - Judge replies: a reply that fails validation is asked for again once with the same request, and the second reply decides. Two invalid replies are masked as judge_unparseable as before. The row keeps the rejected reply with its reason in judge_rejected_responses. - The READMEs say what the judge sees for SWE rows and describe the retry. Validation: gym env test --resources-server partner_if (483 passed); the dispatcher and prepare tests (27 passed); pre-commit. Offline, on all 27 SWE rollouts of the three exit-check runs with a stand-in judge: every judge input fits (largest 841,649), each rollout sends every judged rubric ID in one request, and all 27 are scored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * docs(partner_if): record the 25-task reference score The exit checks on dataset 2.1.0 (25 tasks) pass with GLM 5.3 as policy and judge: reference pass@1[avg-of-3]/all_if_pass 61.33, tolerance 8.5 points (95%), from three runs of 75 rollouts (the other two scored 58.67 and 57.33). It replaces 2.0.0's 62.12, measured on 22 tasks with a 16,384-token cap. The README also says that an endpoint which ends long requests needs a longer per-request timeout, as the runs set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): pin dataset 2.1.4 and validate file blocks Signed-off-by: Arti Jain <artij@nvidia.com> * docs(partner_if): record dataset 2.1.4 regrade score Signed-off-by: Arti Jain <artij@nvidia.com> * docs(partner_if): link the public synthetic examples from the benchmark README The benchmark's rows are internal. Point readers at the resources server's synthetic rows, which use the same format and come with recorded rollouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): leave the S3-mount sandbox label to the deployment The SWE sandbox config set `nemo.nvidia.com/s3-hostpath: "false"` in Gym's public configs, a label only NVIDIA's clusters read. Sandbox labels are deployment-specific, so the public configs keep only `environment: partner-if-swe`, and Frontier Eval's config gives the label to keep the cluster's S3 mount out of the agent's shell. The README drops its sentence about the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): cue the judge's JSON output in its user message partner_if asks its judge for a JSON object (`text.format = json_object`) and says so in the system prompt. Some hosted OpenAI-compatible routes, such as GPT-6 Sol's, accept that format only when the user message itself says JSON. The judge's user payload now ends with `"_judge_output_format": "JSON object with an items array"`, the cue the GPT-6 regrade of dataset 2.1.4 added, so new judge requests match that regrade byte for byte. The manifest moves to 0.2.0, since the 25-task dataset and the judge's input change what scores compare against. The benchmark README says the SWE scores depend on OpenCode 1.17.11, and the dispatcher README no longer says the receipt comes back unchanged: the dispatcher adds the reply's tool executions to the episode's agent observations. The example rollouts are rolled out again with GLM 5.3 as policy and judge, so their judge requests carry the cue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): judge with GPT-6 Sol under judge protocol v5.3 partner_if's judge becomes GPT-6 Sol (`azure/openai/gpt-6-sol`) by default, still through the `vllm_model` judge server at temperature 0 with 32768 output tokens; the endpoint and key stay run settings. The judge protocol moves to `partner-if-evidence-v5.3`, since the previous commit's JSON cue changed the judge's input. The GPT-6 regrade of dataset 2.1.4 recorded its receipts as v5.2 but injected the same cue, so its judge requests match v5.3's bytes. The README names the judge, says the 2.1.4 reference with it is being measured, and keeps the GLM-judged 2.1.0 score as history and the regrade as a cross-check. The example rollouts are rolled out again under v5.3, with GLM 5.3 as policy and judge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * docs(partner_if): record the 2.1.4 reference score with the GPT-6 Sol judge Three full runs of dataset 2.1.4 with GLM 5.3 as the policy and the default GPT-6 Sol judge scored 56.00, 56.00 and 53.33 on `pass@1[avg-of-3]/all_if_pass`, with no masked rollouts. The first is the reference, with a tolerance of ±7.1 points (95%) from the repeat standard deviation pooled over the three runs. The regrade through another route to GPT-6 Sol stays as a cross-check, and the GLM-judged 2.1.0 score as a one-line history note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): leave the policy's sampling to the run The benchmark fixed the policy's sampling for every task at temperature 1.0 and top_p 1.0. It now leaves sampling to the run, which sets its model's sampling on the policy model server, as other benchmarks do. `partner_if_policy_model`, the copy of the run's policy server that every agent calls, keeps that sampling and only removes the output cap, which the long prompts and OpenCode's per-call cap still need. Runs still set no request-level sampling, since opencode_agent refuses it. The README describes the reference run's policy at GLM 5.3's standard sampling (temperature 1.0, top_p 0.95) and notes that the reference score, measured with top_p fixed at 1.0, is being re-measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * docs(partner_if): record the 2.1.4 reference at GLM 5.3's standard sampling With the policy's sampling left to the run, three full runs of dataset 2.1.4 with GLM 5.3 at its standard sampling (temperature 1.0, top_p 0.95) and the default GPT-6 Sol judge scored 57.33, 53.33 and 56.00 on `pass@1[avg-of-3]/all_if_pass`. The first is the reference, with a tolerance of ±9.5 points (95%) from the repeat standard deviation pooled over the three runs. It replaces the interim score measured with top_p fixed at 1.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> * fix(partner_if): keep the episode that closed a SWE session Main's session contract (NVIDIA-NeMo#4679) refuses, with 409, a close that names another episode after the session closed, and the mock-tool server already records the closing episode. The SWE server recorded only that a session closed, so such a close got 200. It now records the episode that closed the session (the seed's episode when a retired attempt cleared it) and refuses a close for any other. A verify that arrives after the close is masked as `session_lost`, since the session's sandbox, and with it the patch, is gone. The contract test now also checks that a close without the cookie releases the sandbox and that the late verify is masked, and two tests mirror the mock-tool server's: a closed or retired session keeps its episode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com> --------- Signed-off-by: Katherine Huang <katherineh@nvidia.com> Signed-off-by: Arti Jain <artij@nvidia.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Arti Jain <artij@nvidia.com>
This was referenced Oct 30, 2026
This was referenced Nov 3, 2026
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.
What changed
This PR tightens the resources sessions contract: the rules every stateful resources server must follow so
single_agent_turncan drive it. It covers four foundational gaps in the environment server migration.ResourcesSessionFenceinnemo_gym/base_resources_server.pygives eachresources_session_idits own lock and remembers a closed session for the server'ssession_close_retry_window_seconds(300 s by default) instead of until the process exits. The records live in main'sClosedResourcesSessionRecords(fix(server): route a session's requests to the worker that holds it #3905), so with several workers a record never outlives the session's routing claim, and a retried close keeps the first record. Each server used to keep its own closed set, which grows by one entry per episode.resources_session_id.ValueError, which becomes a 500, andsingle_agent_turndispatches the whole episode again. Now:ResourcesSeedRejectedError(422) covers input that fails validation, such astask_dataor aTaskIdthat does not match the task row;ResourcesSessionConflictError(409) covers an ID already bound to another episode or task, a close that names another episode, and a seed for a closed session./verifyfor a session the server no longer holds returns a masked sample built bylost_session_verification: reward 0,mask_sample=True,failure_kindsession_lost(orsession_expired). Before, servers returned an unmasked 0, which reads as a policy that solved nothing, or raised an error.ResourcesSessionLostError(404) with afailure_kind, instead of an ordinary tool observation. What a harness does with it varies:simple_agentshows the body to the model and keeps going, Stirrup fails the run so the episode is dispatched again, and an MCP client reports a tool error. The masked verify is what keeps the episode out of the score./verify) does not show up only in coverage metrics.check_resources_session_contractenforces the new rules, and every server that already ran it onmainmoves onto them in this PR, so none of their tests break:example_single_tool_call(the reference server);gdpval;partner_ifmock tools (in-process and sandboxed): closed records retain the episode identity, so a close naming another episode or attempt returns 409 after the session is gone. Retried closes keep the first record and expiry; retirement of an unknown session keeps an ownerless fence;nemo_user_sim. It also gets an ID-to-state map, because the stricter check showed that a second episode could bind the sameresources_session_idand that a cookie-less close missed its seed.swebench_proandinteractive_browsermove onto the contract in the stacked #4692.Fixes found in review, also in this PR:
tag_resultsnow records it as the terminal verification failure the old 409 produced, andaggregate_metricsdoes not count it as an observed stage row.nemo_user_sim: a close of a closed session that names another episode gets 409, and a seed dropped aftersession_ttl_secondsverifies assession_expiredinstead ofsession_lost.The contract
Where each rule applies in an episode
Teal steps are the resources sessions API. The notes show what this PR adds at each step.
sequenceDiagram autonumber participant A as Agent server participant E as Environment server (single_agent_turn) participant R as Resources server rect rgba(14,116,105,0.12) E->>R: POST /seed_session {resources_session_id, episode_id, task_id, task_data} R-->>E: 200 + session cookie (+ MCP tools, sandbox_access) Note over R,E: or 422 bad input / 409 conflict or closed: final, no retry end E->>A: POST /v1/agent_sessions (tool grants carry the cookie) E->>A: POST /v1/responses A->>R: tool calls (with cookie) Note over A,R: session gone: 404 failure_kind=session_lost E->>A: POST /v1/agent_sessions/close E->>R: POST /verify (with cookie) Note over E,R: session gone: 200, reward 0, mask_sample=true, failure_kind=session_lost rect rgba(14,116,105,0.12) E->>R: POST /close_session {resources_session_id, episode_id} Note over R: release by ID (cookie optional), fence the ID for session_close_retry_window_seconds endThe lost-seed-reply path
single_agent_turnregisters the resources close before it sends the seed, so a seed whose reply is lost is still closed. But the environment server never received the cookie, so that close carries an empty cookie jar.sequenceDiagram participant E as Environment server participant R as Resources server Note over E: register close(resources_session_id) first E->>R: POST /seed_session {resources_session_id: "s1"} Note over R: starts sandbox, sets cookie R--xE: reply lost (timeout, disconnect) E->>R: POST /close_session {resources_session_id: "s1"} with no cookie Note over R: find s1's state by ID, release it, fence s1 E->>R: (late retry) POST /seed_session {resources_session_id: "s1"} R-->>E: 409 already closed (not retried)How a resources server answers
The dividing line is whether a retry can change the result.
single_agent_turnretries 5xx, 408, 425 and 429 by dispatching the episode again, and treats every other 4xx as final.flowchart TD Seed["/seed_session"] --> V{"input valid?"} V -->|no: task_data, TaskId| R422["422 ResourcesSeedRejectedError<br/>final"] V -->|yes| C{"ID bound to another episode,<br/>or already closed?"} C -->|yes| R409["409 ResourcesSessionConflictError<br/>final"] C -->|no| P{"provider or dependency failed?"} P -->|yes| R5xx["5xx<br/>episode retried"] P -->|no| OK["200"] Tool["tool call"] --> G1{"session held?"} G1 -->|no| R404["404 ResourcesSessionLostError<br/>failure_kind=session_lost"] Verify["/verify"] --> G2{"session held?"} G2 -->|no| Masked["200 lost_session_verification<br/>reward 0, mask_sample=true"] G2 -->|yes| Score["score the episode"]The rules
/seed_sessionresources_session_idwith the same episode and task returns the existing session (idempotent)./seed_sessionResourcesSeedRejectedError)./seed_sessionResourcesSessionConflictError;ResourcesSessionFence.raise_if_closedraises it)./seed_sessionfailure_kind(ResourcesSessionLostError)./verifylost_session_verification), never a plain 0 and never an error./close_session/close_sessionresources_session_id, even with no cookie./close_sessionsession_close_retry_window_seconds(300 s by default, the same setting agent sessions use), so callers must finish seed retries within that window. A retried close keeps the first close's record and expiry.A server whose score does not depend on per-rollout state, such as
example_single_tool_call, needs no lost-session handling at verify.Using the helpers
Checking a server
With
keeps_state, the check now also requires 409 for a seed that binds the open ID to another episode, for a close of the closed session that names another episode, and for every late seed.TODO: sessions that are never closed (not in this PR)
When the environment server dies before its close, the session stays open. Nothing in the shared code expires it, and provider TTLs don't cover every case: apptainer ignores
ttl_s. The agreed rule, to implement inResourcesSessionFence:ttl_swhen set, otherwise 24 hours, configurable per server;session_expiredsample.Only resources sessions are in scope; agent sessions have the same gap and are tracked separately. The
TODOin theResourcesSessionFencedocstring records this.Relation to other PRs
fix(server): route a session's requests to the worker that holds it #3905 (merged into main): routes each session's requests to the worker that holds it and adds
ClosedResourcesSessionRecordswithsession_close_retry_window_seconds.ResourcesSessionFencekeeps its per-session lock and stores closed records there. On a served resources server, a typed seed's cookie carriesresources_session_id, and a cookie-less close is given that ID too; releasing bybody.resources_session_idis still required for apps without session routing, such as tests.fix(resources): move swebench_pro and interactive_browser onto the session contract #4692 (stacked on this PR) moves
swebench_proandinteractive_browseronto these rules.fix(agent): refuse unsupported agent session seeds with a final 422 #4680 is the agent side:
AgentSeedRejectedError(422) for agent seeds. It also makessingle_agent_turnput the refusing server's reason intofailure_reasonfor every stage, so the 422 and 409 reasons from this PR reach the episode failure.feat(swe): serve typed resources sessions from SWE-Gym and R2E-Gym #4302: its
SandboxSessionResourcesServeralready keys sandboxes byresources_session_idand bounds its fence with its own 3600 s retention. It can replace_closed_task_sessionswithResourcesSessionFence, which keeps the record forsession_close_retry_window_seconds, and useraise_if_closed(409) instead of itsValueError(500). Its open-session expiry is the sandbox-specific version of the TODO above.Linked issues
Follow-ups (not in this PR)
openenvreturns an unmasked 0 at verify when its session is gone. It is still on the legacy/runpath and will get the rule when it migrates (feat(openenv): serve the typed resources sessions API #5483)./runseeds (deepswe,deepswe_external1,job_bench,workspace_bench,toolsandbox) get the refusal rules when they migrate to typed sessions.terminal_bench_2_1andexample_session_state_mgmt/nooa_app.pyserve typed sessions but keep their own closed records, andterminal_bench_2_1answers a verify for a lost session with 409 instead of a masked sample. Both should move ontoResourcesSessionFenceandlost_session_verification.ResourcesSessionFence(record the close before taking the lock, asterminal_bench_2_1does).Validation
Partner IF follow-up at
7ebecb5f759d20c5b00d0413fe525e9a411c078c:pytest resources_servers/partner_if/tests tests/unit_tests/test_base_resources_server.py tests/unit_tests/test_resources_sessions.py --import-mode=importlib -q -o addopts= -o log_cli=false --tb=short: 514 passed. Five additional lifecycle controls passed (failed-stop retry, wrong attempt, first-close expiry, known/unknown retirement). The three added regression cases fail against the originalmock_tools_app.py.pre-commit run --all-filespassed with Python 3.13.14; the private guardword catalog was unavailable, so that hook skipped its private scan.pytest tests/unit_tests/test_base_resources_server.py tests/unit_tests/test_session_contract_repo.py tests/unit_tests/test_resources_sessions.py tests/unit_tests/test_session_routing.py: 120 passed (after merging main). New tests:closed records expire after the window and stay bounded over 1,000 closes; a retried close keeps the first close's episode and expiry;
the lock serializes one session and is dropped when unused, including for a cancelled waiter;
the refusal errors are 422 and 409 and still
ValueErrors;ResourcesSessionLostErroris 404 and names itsfailure_kind;lost_session_verificationbuilds a valid masked response;the contract check fails for a server that:
close left state for ...-lost-seed-reply);It passes for one that follows every rule. It also fails for a server that refuses a late seed with 500, or accepts a close of a closed session that names another episode.
one session's lock does not block another;
clear()forgets closed records;422 and 404 refusals reach the wire through the
SimpleServermiddleware;a masked sample built with the documented merge pattern survives an
extra="allow"request carryingreward, logs a warning, and is kept for coverage and dropped from quality byselect_measured.pytest resources_servers/example_single_tool_call/tests: 6 passed.pytest resources_servers/nullable_tool_args/tests: 4 passed (stateless caller of the check).pytest resources_servers/gdpval/testsin a venv built from itsrequirements.txt: 865 passed, 1 skipped. Includes the contract check withholds_stateandverify_body; tests that pinned 400 orValueErrornow assert 404, 409 or 422, and a verify with no session and no deliverables is a maskedsession_lostsample instead of a 409. New tests: a lost-session row is a terminal verification failure in the multistage orchestrator and does not complete the final stage; a gone session with an explicitdeliverables_diris still judged (not masked);task_datathat is not a task gets 422; a refused seed does not hand out the live session's cookie.pytest resources_servers/nemo_user_sim/testsin a venv withnemo-usersiminstalled: 38 passed. Includes the contract check withholds_stateandverify_body; a closed or unknown session now verifies as a maskedsession_lostsample and an expired one assession_expired, instead of a 404; a close finds the session by ID before the cookie.pytest tests/unit_tests -n 8: 8623 passed, 36 failed, 2 collection errors. All of the failures are in files this PR does not touch (opensandbox compose/cleanup/snapshots, the super35 batch launcher, CI environment, exporters, reward profile, telemetry). The same files fail the same way on unmodifiedorigin/mainin this local environment.pre-commit run --files <changed files>: passed.Rollout evidence
Partner IF follow-up at
7ebecb5f759d20c5b00d0413fe525e9a411c078c: two realSingleAgentTurn→simple_agent→ NIMnvidia/nemotron-3-ultra-550b-a55bepisodes, one in-process and one in a real Docker sandbox, on the shipped greeting task. Each made three model calls, executedlookupandappend, producedHELLO, and received unmasked reward 1 from the stock fixed-rule verifier. Both modes returned 200 for repeated same-episode close and 409 for a different episode or attempt. A second real seed in each mode had its reply replaced with 504 by a test wrapper: the environment closed it without a cookie, left no episode, and late seed returned 409. The Docker containers were removed. This is a bounded lifecycle smoke, not benchmark qualification.The original three server smoke rollouts below have not been run yet; their results remain to be added before merge.
example_single_tool_callundersingle_agent_turnwithsimple_agent: episodes verify and closes return 200.gdpvalundersingle_agent_turn: episodes verify. A verify after a restart is maskedsession_lost, is not judged, and leaves the stage degraded rather than complete.nemo_user_simunder its environment server: closes return 200 and a healthy run has no masked samples.Compatibility
task_datareturns 422, where they used to return 500. A 500 was retried; these are now final.gdpvalandnemo_user_sim. A verify for a lost session is a masked sample, so it counts toward coverage but not quality metrics.gdpvalused to return a 409, andnemo_user_sima 404, which produced no sample. A tool call for a lostgdpvalsession returns 404 instead of 400.ValueErrors, so direct callers that catchValueErrorkeep working.gdpvalseed no longer sets the session cookie. Before, a caller refused with 409 could act on another episode's live session.nemo_user_simstate:closed_resources_session_idsnow holds(EpisodeId, expires_at), and an expired seed verifies assession_expired.check_resources_session_contractis stricter: 409 for conflicting and late seeds where it accepted any 4xx, and 409 for a close of a closed session that names another episode.holds_stateandverify_bodyare optional keyword arguments. Every caller in the repo passes.🤖 Generated with Claude Code