From 515d372a14298db96d7310564d28fc6bbb040ef7 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Fri, 17 Jul 2026 10:34:27 -0700 Subject: [PATCH] fix(compaction): validate retry_in backoff before rendering the retry note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updateCompactionProgress coerced evt.retry_in with Number() and rendered it unguarded, while the sibling part/total path two lines below is finiteness- validated — a malformed backoff would render "retrying in NaNs". Validate retry_in the same way (finite, non-negative), and keep the error text regardless: the error is the load-bearing half of the note, so an unparseable duration drops to "retrying (error)…" rather than suppressing the whole arm. Addresses PR review feedback on the compaction reducer. --- tests/test_conversation_js.py | 14 ++++++++++++++ turnstone/shared_static/conversation.js | 14 +++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/test_conversation_js.py b/tests/test_conversation_js.py index a2d5a04a..48199b30 100644 --- a/tests/test_conversation_js.py +++ b/tests/test_conversation_js.py @@ -168,3 +168,17 @@ def test_unbounded_render_inputs_are_capped() -> None: assert "more preview lines not shown" in body assert "RAW_CAP" in body assert "truncated for display" in body + + +def test_retry_note_validates_backoff_before_rendering() -> None: + """retry_in is a server-emitted backoff coerced with Number(); like the + part/total pair just below it, it must be finiteness-validated (and + non-negative) so a malformed value can't render "retrying in NaNs". The + error text is kept regardless — it is the load-bearing half of the note.""" + body = _body() + assert "Number.isFinite(secs) && secs >= 0" in body, ( + "retry_in must be validated (finite, non-negative) before its seconds render" + ) + assert "Math.round(Number(evt.retry_in))" not in body, ( + "retry_in must not be Math.round(Number(...))'d without a finiteness guard" + ) diff --git a/turnstone/shared_static/conversation.js b/turnstone/shared_static/conversation.js index cf291bd2..44fac487 100644 --- a/turnstone/shared_static/conversation.js +++ b/turnstone/shared_static/conversation.js @@ -169,12 +169,16 @@ export function updateCompactionProgress(el, evt) { return; } if (evt.retry_in != null) { + // retry_in is a server-emitted backoff (seconds) coerced with Number(); + // validate it the way part/total below are, so a malformed value can't + // render "retrying in NaNs". The error text is the load-bearing half — + // keep it whether or not the duration parses. + const secs = Number(evt.retry_in); + const err = String(evt.error || "error"); note.textContent = - "retrying in " + - Math.round(Number(evt.retry_in)) + - "s (" + - String(evt.error || "error") + - ")…"; + Number.isFinite(secs) && secs >= 0 + ? "retrying in " + Math.round(secs) + "s (" + err + ")…" + : "retrying (" + err + ")…"; return; } const part = Number(evt.part);