Skip to content

fix(runstore): rounds/status survive NaN rewards and torn JSONL (#271) - #272

Merged
Birfy merged 6 commits into
mainfrom
fix/issue-271-runstore-rounds-nan-corrupt
Oct 15, 2026
Merged

Birfy merged 6 commits into
mainfrom
fix/issue-271-runstore-rounds-nan-corrupt

Conversation

@Birfy

@Birfy Birfy commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #271.

  • append_round encodes first with allow_nan=False and writes non-finite floats as null (no portable-breaking NaN / Infinity tokens).
  • rounds() skips torn/corrupt JSONL lines instead of raising, so one bad line no longer hides the rest from status / MCP / the panel.
  • on_round (and the final status write) track only finite rewards for best_reward / last_reward, so max(best) cannot become order-dependent on NaN.
  • Loopback _json sanitizes + allow_nan=False so the browser's JSON.parse always gets strict JSON.

Test plan

  • test_append_round_writes_null_not_nan_for_nonfinite_reward fails on main / passes on branch
  • test_rounds_skips_a_torn_line_and_keeps_the_rest fails on main / passes on branch
  • tests/test_runstore_cli.py (26) green locally
  • CI matrix

append_round encodes with allow_nan=False and nulls non-finite floats;
rounds() skips malformed lines; on_round tracks only finite rewards so
best_reward is order-independent; the loopback panel dumps the same way.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T00:15:28.630772Z ce5bb63 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (COMMENT; self-approve blocked)

Closes #271. Verified locally:

  • Diff vs main: append_round encode-first + allow_nan=False + non-finite → null; rounds() skips torn/corrupt lines; on_round / final status track only finite rewards; panel _json sanitizes the same way.
  • Regressions: test_append_round_writes_null_not_nan… and test_rounds_skips_a_torn_line… fail on main / pass on branch; helpers + status/panel strict-JSON tests pass; full tests/test_runstore_cli.py 26 passed.
  • Distinct from #268 (_write_json orphan *.tmp).

Nits (non-blocking): _write_json still allows NaN if some other caller passes a non-finite float into update_status directly; this PR closes the on_round / append_round / panel paths. A follow-up could put allow_nan=False + sanitize inside _write_json once #268 lands.

CI pending on this push.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce5bb63879

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agentdescent/runstore.py
break the status panel's ``JSON.parse``. Encode before opening the
file so a serializer refusal cannot leave a torn partial line.
"""
payload = _sanitize_jsonable(asdict(info))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle null rewards in the text status commands

When a round has a non-finite reward, this conversion stores held_out_reward as null, but both cmd_status and cmd_watch still format that field with :.3f (agentdescent/cli.py lines 535 and 572). Consequently, the ordinary non-JSON status <run_id> and watch <run_id> commands raise TypeError for exactly the NaN/Infinity runs this change is intended to make observable; those consumers need a null-safe display path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f06cdaa. cmd_status (text status <run_id>) and cmd_watch now format held_out_reward through _fmt_reward, which prints a null reward (stored for a non-finite one) as n/a instead of hitting TypeError on :.3f. Regression: test_text_status_and_watch_print_null_rewards drives both commands in-process over a NaN round followed by a finite round. It fails on the previous head with cli.py:535: TypeError and passes now.

Comment thread agentdescent/runstore.py Outdated
line = json.dumps(payload, default=str, allow_nan=False) + "\n"
with open(self.rounds_path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(asdict(info), default=str) + "\n")
fh.write(line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the record boundary after a torn final line

If the process is killed midway through its final write, the torn JSONL fragment normally has no trailing newline. On resume, append mode writes the next valid object directly after that fragment, producing one concatenated invalid line; rounds() then skips both the torn record and the first valid resumed round. Before appending, ensure an existing unterminated tail is separated or truncated so the newly encoded round remains independently readable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f06cdaa. append_round now opens rounds.jsonl in ab+, and when the last byte is not \n it writes a newline before the encoded round, so a torn fragment left by a kill stays its own skipped line instead of swallowing the next round on resume. This is the same repair AuditStore._write_line uses in #277. rounds() also reads bytes and decodes each line, so a tear inside a multi-byte character costs that one line instead of raising UnicodeDecodeError for the whole file. Regressions: test_append_after_a_torn_tail_without_newline_keeps_the_next_round and test_rounds_survives_a_tear_inside_a_multibyte_character. Both fail on the previous head and pass now; tests/test_runstore_cli.py 32 passed.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (COMMENT; self-approve blocked)

Closes #271. Independently verified on ce5bb63:

Correctness

  • append_round: sanitize → json.dumps(..., allow_nan=False) before opening the file — non-finite floats become null, serializer refusal cannot leave a torn partial line.
  • rounds(): skips blank / ValueError (covers JSONDecodeError) / non-dict rows; one bad line no longer hides the rest from status / MCP / panel.
  • on_round + final status: only finite rewards enter best; last_reward / best_reward use _finite_or_none; empty best → None — kills the max([1.0, nan]) vs max([nan, 1.0]) order dependence.
  • Loopback _json: same sanitize + allow_nan=False so browser JSON.parse always gets strict JSON.

Design fit

Matches the package pattern from #264/#268/#270 (encode-first, fail-closed for non-portable tokens). Distinct from #268 (_write_json orphan *.tmp).

Tests

  • 4 regressions fail on main / pass on branch:
    • test_append_round_writes_null_not_nan_for_nonfinite_reward
    • test_rounds_skips_a_torn_line_and_keeps_the_rest
    • test_finite_or_none_and_sanitize_strip_nonfinite_floats
    • test_status_and_panel_payload_stay_strict_json_after_nan_rounds
  • Full tests/test_runstore_cli.py: 26 passed on branch.
  • Smoke: test_runstore_cli + test_evalcache green.

Docs

CHANGELOG Fixed entry is accurate and scoped to #271.

Nits (non-blocking)

  1. _write_json still defaults to allow_nan=True. Callers that pass a non-finite float into update_status directly (bypassing on_round / append_round) can still write NaN into status.json. This PR correctly closes the on_round / append_round / panel paths; a follow-up could put sanitize + allow_nan=False inside _write_json once #268 lands.
  2. Extra blank line before _finite_or_none (cosmetic).
  3. rounds() silently drops non-dict JSON values (arrays/scalars) — intentional and fine; no log/count of skipped lines (acceptable for this scope).

CI matrix still pending on this push at review time.

…e-rounds-nan-corrupt

# Conflicts:
#	CHANGELOG.md
#	tests/test_runstore_cli.py
Resolve CHANGELOG.md: keep #271 runstore entry above newly merged #263 FileCache.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-check after merging main (post-#264/#253)

CHANGELOG conflict resolved by keeping this PR's Fixed entry above the newly merged #263 FileCache note. Exact-head CI (build + 3.9–3.13) is green again; still MERGEABLE. Prior LGTM stands (self-approve blocked → COMMENT).

…e-rounds-nan-corrupt

# Conflicts:
#	CHANGELOG.md

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at a9e23e7: still LGTM. This push only merges current main (1049ab5, after #250/#260/#279 landed); the sole conflict was CHANGELOG.md, resolved by keeping both entries, and the branch diff against main is unchanged apart from that. Full local suite passed and CI is green. Mergeable, no open blocking feedback.

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 8a5ddd6: LGTM (COMMENT, since self-approve is blocked)

f06cdaa addresses the two Codex P2s that were still open on ce5bb63. Each has a thread reply. 8a5ddd6 then merges current main (3947aee); the only conflict was CHANGELOG.md, resolved by keeping both entries.

Text status / watch with null rewards (cli.py)

  • cmd_status (per-run text view) and cmd_watch now format held_out_reward through _fmt_reward: None → n/a, numbers → :.3f, anything else → str. Before this, a NaN round (stored as null by this PR) crashed both commands with TypeError at cli.py:535 / :572. _status_line already handled a None best_reward.

Torn tail without a newline (runstore.py)

  • append_round opens in ab+. If the last byte is not \n, it writes a newline first, so a fragment left by a kill stays its own skipped line instead of swallowing the next round on resume. This mirrors AuditStore._write_line in #277, and encoding still happens before the file is touched.
  • rounds() reads bytes and decodes per line, so a tear inside a multi-byte character costs one line instead of a UnicodeDecodeError for the whole file. This is the same pattern as #274 and #255.

Tests

  • New: test_append_after_a_torn_tail_without_newline_keeps_the_next_round, test_rounds_survives_a_tear_inside_a_multibyte_character, test_text_status_and_watch_print_null_rewards. All 3 fail on the previous head and pass now.
  • tests/test_runstore_cli.py: 32 passed. Every test file touching runstore/cli/rounds() passes, and test_runstore_cli + test_evalcache are green after the merge.

Nit (non-blocking, unchanged from before)

  • _write_json callers other than on_round can still pass a non-finite float into update_status. A follow-up could sanitize there.

Exact-head CI (build + 3.9–3.13) is green and the PR is MERGEABLE. No open blocking feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runstore: one corrupt rounds.jsonl line (or a NaN reward) breaks rounds()/status panel; best_reward=max(best) is order-dependent on NaN

1 participant