Repository navigation
fix(trajectory): an unencodable record is counted, never raised out of record() (#259) - #260
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Birfy
left a comment
There was a problem hiding this comment.
Review
Verdict: LGTM (Closes #259)
Faithful fix for the contract gap: record() promised “a failed write must never fail a rollout,” but only OSError was caught while json.dump(..., allow_nan=False, default=str) and _prepare ran inside the write. Unencodable payloads (NaN/inf, non-string dict keys, circular refs, raising __str__, non-string event detail under record_text=True) escaped and were treated as backend transients inside eval_one’s retry loop — re-running an already-paid rollout ATTEMPTS times.
Correctness
- Encoding (
trajectory_key/_prepare/json.dumps) now happens before any disk touch; failure →trajectory_write_failures++, returnNone, no file / no orphan.tmp. Matches the full-disk degradation path. - Successful path uses
json.dumpsthenfh.write(text)with the same kwargs as before (sort_keys=True,default=str,allow_nan=False), so encodable records stay byte-identical for cross-process dedupe. KeyboardInterrupt/SystemExitstill propagate (except Exception, notBaseException). Good.- Confirmed on
main:record(... output={"logprob": nan})raisesValueError; all 6 new regressions fail on main and pass on this branch.
Tests
- Parametrized unencodable cases + circular + non-string detail + the critical
eval_one“paid once, notATTEMPTS” gate. - Full suite:
pytest -qexit 0 locally (Python 3.13 / venv).
Docs
- Docstring + CHANGELOG entry are clear and match the issue acceptance criteria.
Nits (non-blocking)
- Wrapping
trajectory_keyin the same broadexcept Exceptionis slightly wider than the issue’s encoding story (part.encodeonly fails if a field isn’t astr), but it’s consistent with “recording must never fail a rollout” and harmless. - CI matrix still pending at review time; local green is enough to ship once CI catches up.
Approve when CI is green / with admin bypass as usual. Not merging from this review.
|
Independently verified Direct runtime controls across six unencodable output shapes show old code calling the synthetic provider three times then raising; this head returns the successful reward after one call, counts one recording failure, and leaves the recording root absent. Real sync/async worker drivers also finish successfully for NaN, tuple-key and raising-string outputs: each fixture makes 14 synthetic calls with 14 counted recording failures and no duplicate Additional controls verify no disk/temp mutation on encoding failure, later healthy writes, ten byte-identical old/new serialization comparisons, existing-record deduplication, and unchanged Exact-head tests and docs are terminal green, including Python 3.9–3.13 wheel/install smoke. Local checks used in-process synthetic callables, not paid models, remote workers or live datasets; the local run was focused, not the full suite. No new concrete defect found in this scoped verification; no formal approval is submitted. |
Birfy
left a comment
There was a problem hiding this comment.
Re-check at latest head
Verdict: LGTM (still; Closes #259)
Re-verified after the merge-main commit on head 0f204730:
- Focused regressions (
unencodable/circular/non_string_event_detail/eval_one_does_not_rerun…) → 6 passed pytest tests/test_trajectory.py→ green- CI on this head: tests 3.9–3.13 + docs build SUCCESS (deploy skipped)
Prior LGTM at ae81893 still holds; no new review feedback to address. Ready to merge.
(Posted as COMMENT — author account cannot APPROVE.)
Resolve CHANGELOG.md conflict with recently merged entries.
…-unencodable-payload # Conflicts: # CHANGELOG.md
Resolve CHANGELOG.md: keep #259 TrajectoryStore unencodable record above newly merged main entries.
Closes #259
What
TrajectoryStore.record()only caughtOSError, but serialised the payload withjson.dump(..., default=str, allow_nan=False)inside the write. A record that cannot be encoded (NaN/inf inside a structured output, a dict output with non-string keys, a circular value, an object whose__str__raises, or a non-stringTrajectoryEvent.detailunderrecord_text=True) therefore escapedrecord(), breaking the store's own "a failed write must never fail a rollout" guarantee.The worst consequence was in
_Runtime.eval_one:_record_evalruns inside its retry loop, so the escaped error was read as a backend transient and the already-paid rollout was re-runATTEMPTStimes before the gate failed.Change
trajectory_key,_prepareand the JSON encoding now run before the disk is touched. Any failure there is counted intrajectory_write_failuresandrecord()returnsNone, exactly like a full disk. Nothing is written and no.tmpis left behind.os.replacepath.json.dumpsproduces the same bytesjson.dumpdid, so records that encoded before are byte-identical (the cross-process dedupe property is unchanged).Tests
New regressions in
tests/test_trajectory.py(all 6 fail onmain, pass here):test_unencodable_record_is_counted_not_raised[nan-inside-output|tuple-key|str-raises]: returnsNone, counts one failure, leaves the directory empty, and a later encodable record of the same identity still lands.test_circular_output_is_counted_not_raisedtest_non_string_event_detail_is_counted_not_raisedtest_eval_one_does_not_rerun_a_rollout_whose_record_cannot_be_encoded: the gate returns the reward after oneruncall, notATTEMPTS.Full suite:
pytest -qpasses locally (Python 3.12).