fix(textarena): honor reset(seed) so episodes are reproducible - #1078
fix(textarena): honor reset(seed) so episodes are reproducible#1078adithya-s-k wants to merge 2 commits into
Conversation
TextArenaEnvironment.reset() accepted a `seed` argument but ignored it: it called `self._ta_env.reset(num_players=...)` without seeding, so the Wordle secret word (chosen via the global `random` module) was non-deterministic on every reset. This made the environment non-reproducible and, in particular, broke GRPO-style training where all rollouts in a group must share the same episode (same word) for the group-relative advantage baseline to be valid. TextArena's own reset(seed=...) does not apply the seed to word selection, so we seed the process-global `random` (and `numpy`, if present) immediately before the underlying reset. A lock keeps seed+selection atomic across concurrent sessions, and the prior RNG state is restored afterwards so unseeded sessions sharing the process are undisturbed. The seed is also forwarded to the underlying reset for games/versions that consume it. Adds tests: same seed -> same word (across resets and instances), seed drives selection, unseeded reset still works, and a seeded reset restores global RNG.
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
| """ | ||
| if seed is None: | ||
| self._ta_reset(seed) | ||
| return |
There was a problem hiding this comment.
Unseeded reset skips seed lock
Medium Severity
_seeded_reset only acquires _SEED_LOCK when seed is set, so an unseeded reset (and __init__'s direct _ta_env.reset) can run while another session has temporarily reseeded the process-global RNGs. With SUPPORTS_CONCURRENT_SESSIONS and a thread-pool server, that interleaving can steal draws from a seeded episode or break same-seed reproducibility.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 58723a9. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2131c0f. Configure here.
| """ | ||
| if seed is None: | ||
| self._ta_reset(seed) | ||
| return |
There was a problem hiding this comment.
Unseeded reset skips seed lock
Medium Severity
Unseeded _seeded_reset calls _ta_reset without _SEED_LOCK, so another session can consume the process-global random stream while a seeded reset holds the hijacked RNG. That breaks the atomic seed-plus-selection guarantee and can assign the wrong episode under concurrent sessions.
Reviewed by Cursor Bugbot for commit 2131c0f. Configure here.
| try: | ||
| random.seed(seed) | ||
| if _np is not None: | ||
| _np.random.seed(seed) |
There was a problem hiding this comment.
Large seeds crash with numpy
Low Severity
When numpy is importable, _seeded_reset always calls numpy.random.seed, which rejects integers outside 0..2**32-1. A valid OpenEnv seed that random.seed accepts then raises ValueError and aborts reset, even for games that only use the Python random module.
Reviewed by Cursor Bugbot for commit 2131c0f. Configure here.
There was a problem hiding this comment.
The preservation approach is preferable to a bare seed forward, but this is not concurrency-safe yet. OpenEnv declares SUPPORTS_CONCURRENT_SESSIONS = True, so both seeded and unseeded resets must participate in the same lock. Please add a deterministic interleaving test with the actual TextArena dependency (the repository lock resolves 0.7.4), not an optional test that can silently skip.
Sent by Cursor Automation: Release
| and we restore the prior RNG state afterwards so that unseeded sessions | ||
| sharing the process are left undisturbed. | ||
| """ | ||
| if seed is None: |
There was a problem hiding this comment.
seed=None bypasses _SEED_LOCK, so an unseeded reset can run after another thread calls random.seed(seed) but before it selects/restores. That consumes the seeded stream, perturbs the unseeded session, and can change the seeded episode. Hold the same lock around every underlying reset; for seeded calls, save/seed/reset/restore while holding it. Also, TextArena 0.7.4 already forwards seed through SinglePlayerState.__init__ to State.__init__, which calls random.seed(seed), so update the docstring’s contrary claim.




What
TextArenaEnvironment.reset()accepts aseedargument but never uses it. It callsself._ta_env.reset(num_players=...)without seeding, so the episode (for Wordle, the secret word, chosen via the globalrandommodule) is non-deterministic on every reset. The client -> HTTP -> server plumbing already forwardsseedintoenvironment.reset(seed=...); only the wrapper was dropping it.This makes the environment non-reproducible, and it specifically breaks GRPO-style training: all rollouts in a group must share the same episode (same word) for the group-relative advantage baseline to be meaningful. Today each rollout in a group draws a different word.
Repro (before)
Fix
TextArena's own
reset(seed=...)does not apply the seed to word selection (Wordle usesrandom.choice(word_list)), so the wrapper seeds the process-globalrandom(andnumpyif present) immediately before the underlying reset:seed=Nonekeeps the existing random behaviour.Tests
tests/envs/test_textarena_seed.py(skipped iftextarenaisn't installed):Validated locally with textarena 0.7.4: 5 passed.
ruff format/ruff checkclean.Note
Medium Risk
Seeded resets mutate global RNG state under a lock; incorrect restoration could affect concurrent unseeded sessions, though the implementation explicitly saves and restores state in a
finallyblock.Overview
TextArenaEnvironment.reset(seed=...)is now honored so Wordle (and similar) episodes are reproducible—required for GRPO-style rollouts that must share the same episode within a group.reset()routes through new_seeded_reset: when a seed is set, the wrapper temporarily seeds process-globalrandomandnumpy(if installed), calls the underlying reset (forwardingseedwhen supported via_ta_reset), then restores prior RNG state. A process-wide_SEED_LOCKkeeps seed+selection atomic for concurrent sessions;seed=Nonekeeps prior unseeded behavior.Adds
tests/envs/test_textarena_seed.py(skipped withouttextarena) covering same-seed determinism across resets/instances, seed-driven word variety, unseeded resets, and no leakage into the global RNG stream.Reviewed by Cursor Bugbot for commit 2131c0f. Bugbot is set up for automated code reviews on this repo. Configure here.