Repository navigation
fix(tbench2_env): treat a nan or inf reward.txt as no verdict - #1570
Merged
burtenshaw merged 3 commits intoNov 9, 2026
Merged
Conversation
_parse_canonical_reward returns None for an empty or non-numeric reward.txt, and the callers raise so the episode is reported as a scoring error instead of a reward. float() also accepts "nan", "inf" and "-inf", so those values went through as the reward, and a NaN or infinite reward reached the trainer. Return None for a value that is not finite, the same rule the coding_tools, jupyter and terminus envs already apply to their reward files. Tests cover the local and the Docker evaluate paths.
burtenshaw
reviewed
Nov 9, 2026
burtenshaw
left a comment
Collaborator
There was a problem hiding this comment.
This is ready to merge. A nan or inf in reward.txt now takes the same "no verdict" path as an empty or non-numeric value.
Optional
envs/tbench2_env/server/tbench2_env_environment.py:244: the error still says "reward.txt missing after tests/test.sh ran", which reads wrong when reward.txt holdsnan. Main already says this for non-numeric values, so it is not new. The log tail in the message also shows the marker line. If you want to touch it, "reward.txt missing or not a finite number" covers every case, and the tests match on "produced no verdict", so they still pass.
What I checked
- On main,
_parse_canonical_rewardreturnsnan,infand-infas rewards. Anevaluatestep withnanin reward.txt returnsreward=nan,done=True,success=Trueand no error. Over the WebSocket, pydantic writes the NaN as"reward": nullnext to"done": true. A remote trainer sees a finished episode with no reward and no error, and an in-process caller gets the NaN itself. On this branch the same step returnssuccess=False,done=False,reward=Noneand the "produced no verdict" error, like the existing missing-verdict case. mathis already imported in the module. The samemath.isfinitecheck already exists in_parse_reward_overridein coding_tools_env, jupyter_env and terminus_env.PYTHONPATH=src:envs pytest tests/envs/test_tbench2_env.py -q: 67 passed, 1 skipped on this branch. The skip is the opt-in smoke test (TB2_ENABLE_TESTSnot set). Main with your test file: the 4 new cases fail withDID NOT RAISE, and the rest pass.- The branch merges cleanly onto current main, and the same tests pass there.
ruff format --checkandruff checkare clean on both changed files.usort checkreports the same finding in the env module on main.
Reproduction
Run from the repo root with PYTHONPATH=src:envs python repro.py. It points the verifier paths at a temp dir.
import tempfile
from pathlib import Path
from openenv.core.env_server.serialization import serialize_observation
from openenv.core.env_server.types import WSObservationResponse
from tbench2_env.models import Tbench2Action
from tbench2_env.server import tbench2_env_environment as mod
for raw in ["1", "", "abc", "nan", "inf", "-inf"]:
print(f"{raw!r}: {mod._parse_canonical_reward(f'__TB2_REWARD__:{raw}')!r}")
class Toolkit:
def shell_exec(self, **kwargs):
return "__TB2_REWARD__:nan"
root = Path(tempfile.mkdtemp())
mod._VERIFY_TESTS_DIR = str(root / "tests")
mod._VERIFIER_LOG_DIR = str(root / "logs")
(root / "task" / "tests").mkdir(parents=True)
(root / "task" / "tests" / "test.sh").write_text("")
env = mod.Tbench2Environment(withhold_tests=False)
env._task_dir, env._workdir, env._terminal_toolkit = root / "task", str(root), Toolkit()
obs = env.step(Tbench2Action(action_type="evaluate"))
print(obs.reward, obs.done, obs.success, repr(obs.error[:44]))
print(WSObservationResponse(data=serialize_observation(obs)).model_dump_json()[-30:])main:
'1': 1.0
'': None
'abc': None
'nan': nan
'inf': inf
'-inf': -inf
nan True True ''
}},"reward":null,"done":true}}
this branch:
'1': 1.0
'': None
'abc': None
'nan': None
'inf': None
'-inf': None
None False False 'canonical harness produced no verdict (rewar'
},"reward":null,"done":false}}
Contributor
Author
|
Thanks for the careful review. I took the wording: b55de2c changes the message to "reward.txt missing or not a finite number". The tests still pass (67 passed, 1 skipped), and ruff is clean. |
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
_parse_canonical_rewardintbench2_envreturnsNonefor an empty or non-numericreward.txt, and the callers raise so the episode is reported as a scoring error. Butfloat()also acceptsnan,infand-inf, so areward.txtholding one of those went through as the reward. This makes a value that is not finite count as no verdict too.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
Details
Since #1025, a missing verdict raises in
_require_canonical_verdict, so RL consumers can drop the episode instead of training on a false 0.0. On main,__TB2_REWARD__:nanis parsed asfloat("nan"),_evaluate_taskand_evaluate_dockerreturnreward=nanwithtests_passed: False, and the NaN goes to the trainer.infgoes through the same way.The other envs that read a reward file already reject this case:
_parse_reward_overrideincoding_tools_env,jupyter_envandterminus_envreturns no reward whenmath.isfinite(value)is false. This applies the same check in_parse_canonical_reward, so both the local and the Docker path raise "canonical harness produced no verdict", the same as for an emptyreward.txt. I also updated the docstring.Test Plan
Two tests in
tests/envs/test_tbench2_env.py, next to the missing-verdict tests from #1025:test_evaluate_non_finite_verdict_raises, fornan,infand-infon the local path. It also checks that the staged tests and the verifier dir are still wiped.test_evaluate_docker_non_finite_verdict_raisesfornanon the Docker path, with the fake container.Run with
PYTHONPATH=src:envs pytest tests/envs/test_tbench2_env.py:DID NOT RAISE), the existing verdict tests pass.The three checks from
.claude/hooks/lint.sh(usort check,ruff format --check,ruff check) are clean on the files this PR touches; the findings a tree-wide run reports are in other files and are the same on main.Note
Low Risk
Scoring-only guard on reward parsing; finite 0.0/1.0 behavior unchanged and covered by new tests.
Overview
TB2 canonical scoring used to treat
reward.txtvalues likenan,inf, and-infas real rewards becausefloat()accepts them._parse_canonical_rewardnow returns no verdict when the parsed value is not finite (math.isfinite), matching how other envs handle reward overrides.When that happens, local and Docker evaluation still fail the same way as a missing or empty verdict:
_require_canonical_verdictraises with an updated message (“missing or not a finite number”) so trainers get a scoring error instead of propagating NaN/inf into RL loops.Tests cover
nan/inf/-infon the local path andnanin Docker mode.Reviewed by Cursor Bugbot for commit 67ce3c3. Bugbot is set up for automated code reviews on this repo. Configure here.