Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633) (#3853)
* fix(streaming): normalize inline thinking extraction across live and persisted turns (#3599)
# Conflicts:
# api/streaming.py
# static/messages.js
# static/ui.js
* fix(streaming): code-aware inline-thinking extraction + position-aware unclosed handling
Codex deep-review caught two regressions in the leading-only -> full-scan
rewrite (both silent data-mangling on the persist/reload path):
1. Code-span unawareness: the scanner only protected triple fences, so a
literal <think> in an inline single-backtick code span or an indented
(>=4-space/tab) code block got silently extracted into reasoning. Added
_inline_thinking_indented_code_at + inline-backtick tracking (Python +
the JS twin _thinkingIndentedCodeAt), so all three code contexts now keep
thinking tags visible.
2. Unclosed-tag truncation: any unmatched open tag moved the trailing prose
into reasoning. Now position-aware — a LEADING unclosed block (cut off
mid-thought) is still reasoning (#3455 intent), but an unclosed tag AFTER
visible content stays visible so literal typed tags don't truncate prose.
Gated partial handling on the previously-unused options.streaming param
(live streaming keeps 'still thinking' behavior; persist/reload does not).
Updated 2 tests that pinned the buggy behavior + added 4 regression tests
(inline-backtick, indented-code, mid-body-unclosed-visible, leading-unclosed-
extracted). Updated the node driver harness to include the new helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): recognize fenced code blocks indented 1-3 spaces
Codex round-3: a fence indented 1-3 spaces is valid Markdown but the fence
detector only matched at column 0, so a literal think tag inside such a fence
(not 4+-space indented code either) was still extracted. Both detectors
(_inline_thinking_fence_marker_at / _thinkingFenceMarkerAt) now walk back over
up to 3 leading spaces to a line start. Added backtick + tilde indented-fence
regression tests.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): O(n) inline-thinking scan + merge separate reasoning on reload
Round-4 Codex deep-review caught two real issues in my own fixes:
1. PERF (O(n^2)): the indented-code check (_inline_thinking_indented_code_at /
_thinkingIndentedCodeAt) scanned to line boundaries at EVERY character index,
plus the leading check sliced+stripped the whole prefix per unclosed tag. On
long no-newline content this was quadratic (~8.4s @ 200k, called repeatedly
on the streaming path). Replaced with incremental O(1)-per-iteration line
state (_line_is_indented_code / _lineIsIndentedCode evaluated only at line
starts) + a seen_nonspace flag. 200k now extracts in ~55-140ms.
2. RELOAD reasoning-drop: renderMessages() seeded the shared extractor with ''
so a message with BOTH an inline <think> block AND a separate m.reasoning
payload showed only the inline part — the separate payload was dropped
because the !thinkingText worklog resolution was then skipped. Now seeds with
the message's direct reasoning (m.reasoning_content||m.reasoning||...) so the
two MERGE (deduped); separate-only reasoning is preserved without promoting
it into visible prose.
Python + JS twins kept line-for-line parity. Added merge + perf + reload
regression tests; updated the reload structure test and the node driver harness
for the renamed helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): revert reload reasoning-seed; keep O(n) perf fix
Codex round-4 finding #2 (seed renderMessages' inline extractor with
m.reasoning so a separate payload merges) turned out to VIOLATE a deliberate
architectural invariant pinned by test_issue2565 +
test_sprint42: the reload content-extraction path must NOT touch
m.reasoning/m.reasoning_content — reasoning metadata is owned exclusively by
the Worklog Thinking Card path (_worklogReasoningTextFromMessage /
_assistantReasoningPayloadText), never conflated with inline-content
extraction (which would risk promoting provider reasoning into final-answer
prose). Reverted the ui.js seed to the PR's original `thinkingText` arg.
The inline+separate merge is still a genuine extractor capability (exercised
by the live streaming path via liveReasoningText) and is covered by a unit
test, just not invoked from the reload render path by design.
The O(n) perf fix (finding #1) and the code-awareness + position-aware
unclosed handling (rounds 1-3) are all retained.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): only lstrip extracted content when a leading block was removed
Codex round-5 catch: the extractor unconditionally lstripped the final content
(.lstrip() / .replace(/^\s+/,'')) even when NO thinking block was extracted, so
an assistant reply that legitimately starts with an indented code block or blank
lines lost its leading whitespace on live display, reload, and persistence. This
was a real regression vs master (master returned non-thinking content unchanged).
Now track leading_removed (set only when a LEADING thinking block/prefix is
actually extracted) and lstrip only in that case. Mid-body / no-thinking content
keeps its exact leading whitespace. Python + JS twins kept in parity; added
backend regression tests (indented-first preserved, leading-blank preserved,
leading-think still strips).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): reconnect restore prefers raw inflight accumulator
Codex round-6 CORE catch: on reconnect, the single-live-message restore used
(_liveInflightAssistant.content || ''). Because the PR now splits a leading
unclosed <think> into empty content, restoring from the split content dropped
the open tag — so a later </think> token leaked into the visible reply and
corrupted the live accumulator. Restore from
(_fullInflightAssistant || _liveInflightAssistant.content || '') so the raw
open tag survives reconnect and the accumulator stays correct. Added a
reconnect-restore regression test.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633)
Unify inline-thinking (<think>/<|channel>/<|turn|>) extraction across live,
reload, and persisted turns (#3599/#3633, @rodboev). Deep-reviewed: Opus +
6 Codex rounds; maintainer fixes resolved every Codex finding — code-awareness
(inline-backtick/indented/1-3-space fences keep literal tags visible),
position-aware unclosed handling, O(n) line scanning (was O(n^2) on long
content), conditional lstrip (preserve leading whitespace when no leading block
removed), and a reconnect-restore CORE fix (raw accumulator preferred so an open
<think> tag survives reconnect). Python + JS twins in parity. Full suite 8330,
Opus SHIP-SAFE, Codex SAFE-TO-SHIP, ESLint/scope-undef/ruff clean.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-09 00:28:05 +00:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
Release v0.51.336 — Release KZ (fix per-token inline-thinking perf regression, #3633 follow-up) (#3854)
* fix(streaming): make per-token inline-thinking extraction linear (#3633 follow-up)
Codex post-merge perf catch on #3633: _parseStreamState() and
syncInflightAssistantMessage() call _extractInlineThinkingFromContent on the
FULL accumulated assistantText on EVERY streamed token. The #3633 rewrite made
that a full char-by-char walk, so cost was O(n^2) over a stream — a Node harness
measured ~88s (no-tag) / ~103s (leading <think> block) for 2000x100-char tokens,
which would freeze the main thread on long reasoning-model responses.
Two fixes (Python api/streaming.py + JS static/messages.js twin, line-for-line parity):
1. Fast path: if the text contains no complete thinking opener AND (when
streaming) its tail is not a prefix of an opener, return unchanged without the
char walk — two cheap substring scans. Handles the common no-tag case.
2. Bulk-skip plain trailing content: track the next complete opener via
str.find/indexOf (_next_inline_thinking_opener / _nextThinkingOpener); once no
opener remains ahead, append the remainder and stop instead of walking it
(streaming still suppresses a trailing partial-opener prefix). Handles the
leading-block-then-long-answer case.
Result: ~88s/103s → ~0.5s/0.9s (Python), ~0.18s/0.21s (JS). All behavioral cases
(persist + streaming, code-awareness, position-aware unclosed, leading whitespace)
verified unchanged in both twins. Added a per-token streaming perf regression test
and wired _nextThinkingOpener into the node driver harness.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): perf bulk-skip must respect code context for partial-opener tails
Codex catch on the perf fix: the no-complete-opener bulk-skip suppressed a
trailing partial opener (e.g. '<thi') unconditionally during streaming, but a
partial opener INSIDE inline-backtick / fenced / indented code must stay visible
(master parity). Now, when streaming and the tail is a partial opener, fall
through to the code-aware char walk (bounded — a partial tail is a transient
single token) instead of bulk-skipping; only a PLAIN-text partial opener is
suppressed as a forming block. Added _text_tail_is_partial_opener /
_textTailIsPartialOpener (Python + JS parity) + regression tests for the
inside-code vs plain partial-tail cases.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs(changelog): v0.51.336 KZ — inline-thinking streaming perf fix
---------
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
2026-06-09 01:16:52 +00:00
|
|
|
from api.streaming import (
|
|
|
|
|
_extract_inline_thinking_from_content,
|
|
|
|
|
_split_thinking_from_content,
|
|
|
|
|
)
|
Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633) (#3853)
* fix(streaming): normalize inline thinking extraction across live and persisted turns (#3599)
# Conflicts:
# api/streaming.py
# static/messages.js
# static/ui.js
* fix(streaming): code-aware inline-thinking extraction + position-aware unclosed handling
Codex deep-review caught two regressions in the leading-only -> full-scan
rewrite (both silent data-mangling on the persist/reload path):
1. Code-span unawareness: the scanner only protected triple fences, so a
literal <think> in an inline single-backtick code span or an indented
(>=4-space/tab) code block got silently extracted into reasoning. Added
_inline_thinking_indented_code_at + inline-backtick tracking (Python +
the JS twin _thinkingIndentedCodeAt), so all three code contexts now keep
thinking tags visible.
2. Unclosed-tag truncation: any unmatched open tag moved the trailing prose
into reasoning. Now position-aware — a LEADING unclosed block (cut off
mid-thought) is still reasoning (#3455 intent), but an unclosed tag AFTER
visible content stays visible so literal typed tags don't truncate prose.
Gated partial handling on the previously-unused options.streaming param
(live streaming keeps 'still thinking' behavior; persist/reload does not).
Updated 2 tests that pinned the buggy behavior + added 4 regression tests
(inline-backtick, indented-code, mid-body-unclosed-visible, leading-unclosed-
extracted). Updated the node driver harness to include the new helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): recognize fenced code blocks indented 1-3 spaces
Codex round-3: a fence indented 1-3 spaces is valid Markdown but the fence
detector only matched at column 0, so a literal think tag inside such a fence
(not 4+-space indented code either) was still extracted. Both detectors
(_inline_thinking_fence_marker_at / _thinkingFenceMarkerAt) now walk back over
up to 3 leading spaces to a line start. Added backtick + tilde indented-fence
regression tests.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): O(n) inline-thinking scan + merge separate reasoning on reload
Round-4 Codex deep-review caught two real issues in my own fixes:
1. PERF (O(n^2)): the indented-code check (_inline_thinking_indented_code_at /
_thinkingIndentedCodeAt) scanned to line boundaries at EVERY character index,
plus the leading check sliced+stripped the whole prefix per unclosed tag. On
long no-newline content this was quadratic (~8.4s @ 200k, called repeatedly
on the streaming path). Replaced with incremental O(1)-per-iteration line
state (_line_is_indented_code / _lineIsIndentedCode evaluated only at line
starts) + a seen_nonspace flag. 200k now extracts in ~55-140ms.
2. RELOAD reasoning-drop: renderMessages() seeded the shared extractor with ''
so a message with BOTH an inline <think> block AND a separate m.reasoning
payload showed only the inline part — the separate payload was dropped
because the !thinkingText worklog resolution was then skipped. Now seeds with
the message's direct reasoning (m.reasoning_content||m.reasoning||...) so the
two MERGE (deduped); separate-only reasoning is preserved without promoting
it into visible prose.
Python + JS twins kept line-for-line parity. Added merge + perf + reload
regression tests; updated the reload structure test and the node driver harness
for the renamed helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): revert reload reasoning-seed; keep O(n) perf fix
Codex round-4 finding #2 (seed renderMessages' inline extractor with
m.reasoning so a separate payload merges) turned out to VIOLATE a deliberate
architectural invariant pinned by test_issue2565 +
test_sprint42: the reload content-extraction path must NOT touch
m.reasoning/m.reasoning_content — reasoning metadata is owned exclusively by
the Worklog Thinking Card path (_worklogReasoningTextFromMessage /
_assistantReasoningPayloadText), never conflated with inline-content
extraction (which would risk promoting provider reasoning into final-answer
prose). Reverted the ui.js seed to the PR's original `thinkingText` arg.
The inline+separate merge is still a genuine extractor capability (exercised
by the live streaming path via liveReasoningText) and is covered by a unit
test, just not invoked from the reload render path by design.
The O(n) perf fix (finding #1) and the code-awareness + position-aware
unclosed handling (rounds 1-3) are all retained.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): only lstrip extracted content when a leading block was removed
Codex round-5 catch: the extractor unconditionally lstripped the final content
(.lstrip() / .replace(/^\s+/,'')) even when NO thinking block was extracted, so
an assistant reply that legitimately starts with an indented code block or blank
lines lost its leading whitespace on live display, reload, and persistence. This
was a real regression vs master (master returned non-thinking content unchanged).
Now track leading_removed (set only when a LEADING thinking block/prefix is
actually extracted) and lstrip only in that case. Mid-body / no-thinking content
keeps its exact leading whitespace. Python + JS twins kept in parity; added
backend regression tests (indented-first preserved, leading-blank preserved,
leading-think still strips).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): reconnect restore prefers raw inflight accumulator
Codex round-6 CORE catch: on reconnect, the single-live-message restore used
(_liveInflightAssistant.content || ''). Because the PR now splits a leading
unclosed <think> into empty content, restoring from the split content dropped
the open tag — so a later </think> token leaked into the visible reply and
corrupted the live accumulator. Restore from
(_fullInflightAssistant || _liveInflightAssistant.content || '') so the raw
open tag survives reconnect and the accumulator stays correct. Added a
reconnect-restore regression test.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633)
Unify inline-thinking (<think>/<|channel>/<|turn|>) extraction across live,
reload, and persisted turns (#3599/#3633, @rodboev). Deep-reviewed: Opus +
6 Codex rounds; maintainer fixes resolved every Codex finding — code-awareness
(inline-backtick/indented/1-3-space fences keep literal tags visible),
position-aware unclosed handling, O(n) line scanning (was O(n^2) on long
content), conditional lstrip (preserve leading whitespace when no leading block
removed), and a reconnect-restore CORE fix (raw accumulator preferred so an open
<think> tag survives reconnect). Python + JS twins in parity. Full suite 8330,
Opus SHIP-SAFE, Codex SAFE-TO-SHIP, ESLint/scope-undef/ruff clean.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-09 00:28:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
|
|
|
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
|
|
|
|
|
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
WORKSPACE_JS = (REPO / "static" / "workspace.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _function_body(src: str, signature: str) -> str:
|
|
|
|
|
start = src.index(signature)
|
|
|
|
|
brace = src.index("{", start)
|
|
|
|
|
depth = 0
|
|
|
|
|
for i in range(brace, len(src)):
|
|
|
|
|
if src[i] == "{":
|
|
|
|
|
depth += 1
|
|
|
|
|
elif src[i] == "}":
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
return src[start : i + 1]
|
|
|
|
|
raise AssertionError(f"function body not found: {signature}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_clean_leading_think_block():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>plan</think>\nanswer")
|
|
|
|
|
|
|
|
|
|
assert content == "answer"
|
|
|
|
|
assert reasoning == "plan"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_extracts_non_leading_complete_block():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("visible before <think>hidden</think> visible after")
|
|
|
|
|
|
|
|
|
|
assert "<think>" not in content
|
|
|
|
|
assert "visible before" in content
|
|
|
|
|
assert "visible after" in content
|
|
|
|
|
assert reasoning == "hidden"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_extracts_multiple_complete_blocks():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>one</think><think>two</think> final")
|
|
|
|
|
|
|
|
|
|
assert content == "final"
|
|
|
|
|
assert reasoning == "one\n\ntwo"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_keeps_fenced_code_literal_think_visible():
|
|
|
|
|
raw = "```html\n<think>literal</think>\n```\nanswer"
|
|
|
|
|
content, reasoning = _split_thinking_from_content(raw)
|
|
|
|
|
|
|
|
|
|
assert content == raw
|
|
|
|
|
assert reasoning == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_merges_existing_reasoning_without_duplicate():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>same</think>answer", "same")
|
|
|
|
|
|
|
|
|
|
assert content == "answer"
|
|
|
|
|
assert reasoning == "same"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_split_merges_existing_reasoning_with_new_inline_block():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>inline</think>answer", "separate")
|
|
|
|
|
|
|
|
|
|
assert content == "answer"
|
|
|
|
|
assert reasoning == "separate\n\ninline"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reasoning_only_content_survives_reload_source_fields():
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>only reasoning</think>")
|
|
|
|
|
|
|
|
|
|
assert content == ""
|
|
|
|
|
assert reasoning == "only reasoning"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unclosed_inline_thinking_after_content_stays_visible_on_persist():
|
|
|
|
|
"""#3633 deep-review (Codex catch): on the PERSIST path an unclosed think tag
|
|
|
|
|
that appears AFTER visible content is almost always a literal typed tag, so
|
|
|
|
|
the prose after it must NOT be silently truncated into reasoning. A LEADING
|
|
|
|
|
unclosed block (cut off mid-thought) is still treated as reasoning."""
|
|
|
|
|
# Mid-body unclosed → stays fully visible, nothing moved to reasoning.
|
|
|
|
|
content, reasoning = _split_thinking_from_content("answer<think>still thinking")
|
|
|
|
|
assert content == "answer<think>still thinking"
|
|
|
|
|
assert reasoning == ""
|
|
|
|
|
|
|
|
|
|
# Leading unclosed → genuine cut-off thinking trace, moves to reasoning.
|
|
|
|
|
lead_content, lead_reasoning = _split_thinking_from_content("<think>still thinking")
|
|
|
|
|
assert lead_content == ""
|
|
|
|
|
assert lead_reasoning == "still thinking"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_messages_js_live_and_persist_paths_share_extractor():
|
|
|
|
|
stream_display = _function_body(MESSAGES_JS, "function _streamDisplay")
|
|
|
|
|
parse_state = _function_body(MESSAGES_JS, "function _parseStreamState")
|
|
|
|
|
split_persist = _function_body(MESSAGES_JS, "function _splitThinkFromContent")
|
|
|
|
|
|
|
|
|
|
assert "_extractInlineThinkingFromContent(_stripXmlToolCalls(assistantText), liveReasoningText, {streaming:true}).content" in stream_display
|
|
|
|
|
assert "return _extractInlineThinkingFromContent(_stripXmlToolCalls(assistantText), liveReasoningText, {streaming:true});" in parse_state
|
|
|
|
|
assert "return _extractInlineThinkingFromContent(rawContent, existingReasoning, {streaming:false});" in split_persist
|
|
|
|
|
assert "window._extractInlineThinkingFromContentForRender" in MESSAGES_JS
|
|
|
|
|
assert "_thinkingFenceMarkerAt" in MESSAGES_JS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_render_messages_uses_shared_extractor_on_reload():
|
|
|
|
|
render = _function_body(UI_JS, "function renderMessages")
|
|
|
|
|
|
|
|
|
|
assert "window._extractInlineThinkingFromContentForRender(content, thinkingText)" in render
|
|
|
|
|
assert "thinkingText=split.reasoning||thinkingText" in render
|
|
|
|
|
assert "content=split.content" in render
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_inline_and_separate_reasoning_merge_not_drop():
|
|
|
|
|
"""#3633: the extractor MERGES inline + an explicitly-passed separate reasoning
|
|
|
|
|
payload (deduped) rather than dropping either. (The reload render path itself
|
|
|
|
|
deliberately does NOT seed m.reasoning into this extractor — that separation
|
|
|
|
|
is pinned by test_issue2565; the merge capability is exercised by the live
|
|
|
|
|
streaming path which passes liveReasoningText.)"""
|
|
|
|
|
content, reasoning = _split_thinking_from_content("<think>inline</think>answer", "separate")
|
|
|
|
|
assert content == "answer"
|
|
|
|
|
assert reasoning == "separate\n\ninline"
|
|
|
|
|
|
|
|
|
|
# Identical inline + separate dedupe to one.
|
|
|
|
|
content2, reasoning2 = _split_thinking_from_content("<think>same</think>answer", "same")
|
|
|
|
|
assert content2 == "answer"
|
|
|
|
|
assert reasoning2 == "same"
|
|
|
|
|
|
|
|
|
|
# Separate-only (no inline tag) is preserved and content is untouched
|
|
|
|
|
# (no promotion of reasoning into visible prose).
|
|
|
|
|
content3, reasoning3 = _split_thinking_from_content("plain answer", "separate")
|
|
|
|
|
assert content3 == "plain answer"
|
|
|
|
|
assert reasoning3 == "separate"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_extraction_is_linear_on_long_no_newline_content():
|
|
|
|
|
"""#3633 Codex perf catch: the indented-code / leading checks must not be
|
|
|
|
|
O(n^2). A 200k-char no-newline message must extract well under a second."""
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
big = "x" * 200_000 + "answer"
|
|
|
|
|
start = time.time()
|
|
|
|
|
content, reasoning = _split_thinking_from_content(big)
|
|
|
|
|
elapsed = time.time() - start
|
|
|
|
|
assert content == big
|
|
|
|
|
assert reasoning == ""
|
|
|
|
|
assert elapsed < 1.0, f"extraction took {elapsed:.2f}s — likely quadratic"
|
|
|
|
|
|
|
|
|
|
|
Release v0.51.336 — Release KZ (fix per-token inline-thinking perf regression, #3633 follow-up) (#3854)
* fix(streaming): make per-token inline-thinking extraction linear (#3633 follow-up)
Codex post-merge perf catch on #3633: _parseStreamState() and
syncInflightAssistantMessage() call _extractInlineThinkingFromContent on the
FULL accumulated assistantText on EVERY streamed token. The #3633 rewrite made
that a full char-by-char walk, so cost was O(n^2) over a stream — a Node harness
measured ~88s (no-tag) / ~103s (leading <think> block) for 2000x100-char tokens,
which would freeze the main thread on long reasoning-model responses.
Two fixes (Python api/streaming.py + JS static/messages.js twin, line-for-line parity):
1. Fast path: if the text contains no complete thinking opener AND (when
streaming) its tail is not a prefix of an opener, return unchanged without the
char walk — two cheap substring scans. Handles the common no-tag case.
2. Bulk-skip plain trailing content: track the next complete opener via
str.find/indexOf (_next_inline_thinking_opener / _nextThinkingOpener); once no
opener remains ahead, append the remainder and stop instead of walking it
(streaming still suppresses a trailing partial-opener prefix). Handles the
leading-block-then-long-answer case.
Result: ~88s/103s → ~0.5s/0.9s (Python), ~0.18s/0.21s (JS). All behavioral cases
(persist + streaming, code-awareness, position-aware unclosed, leading whitespace)
verified unchanged in both twins. Added a per-token streaming perf regression test
and wired _nextThinkingOpener into the node driver harness.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): perf bulk-skip must respect code context for partial-opener tails
Codex catch on the perf fix: the no-complete-opener bulk-skip suppressed a
trailing partial opener (e.g. '<thi') unconditionally during streaming, but a
partial opener INSIDE inline-backtick / fenced / indented code must stay visible
(master parity). Now, when streaming and the tail is a partial opener, fall
through to the code-aware char walk (bounded — a partial tail is a transient
single token) instead of bulk-skipping; only a PLAIN-text partial opener is
suppressed as a forming block. Added _text_tail_is_partial_opener /
_textTailIsPartialOpener (Python + JS parity) + regression tests for the
inside-code vs plain partial-tail cases.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs(changelog): v0.51.336 KZ — inline-thinking streaming perf fix
---------
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
2026-06-09 01:16:52 +00:00
|
|
|
def test_per_token_streaming_scan_is_not_quadratic():
|
|
|
|
|
"""#3633 Codex CORE perf catch: _parseStreamState / syncInflightAssistantMessage
|
|
|
|
|
call the extractor on the FULL accumulator on every streamed token. Simulate a
|
|
|
|
|
long stream (both no-tag and leading-thinking-block cases) and assert the
|
|
|
|
|
cumulative cost stays bounded — a per-token full walk over the growing buffer
|
|
|
|
|
was O(n^2) (~88s no-tag / ~103s with-tag for 2000x100-char tokens)."""
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
def sim(n_tokens, tok_len, lead_tag):
|
|
|
|
|
acc = "<think>short reasoning</think>" if lead_tag else ""
|
|
|
|
|
start = time.time()
|
|
|
|
|
for _ in range(n_tokens):
|
|
|
|
|
acc += "x" * tok_len
|
|
|
|
|
# mimic the two per-token extractor calls (_streamDisplay + _parseStreamState)
|
|
|
|
|
_extract_inline_thinking_from_content(acc, "", streaming=True)
|
|
|
|
|
_extract_inline_thinking_from_content(acc, "", streaming=True)
|
|
|
|
|
return time.time() - start
|
|
|
|
|
|
|
|
|
|
no_tag = sim(2000, 100, False)
|
|
|
|
|
with_tag = sim(2000, 100, True)
|
|
|
|
|
assert no_tag < 3.0, f"no-tag per-token stream took {no_tag:.1f}s — quadratic"
|
|
|
|
|
assert with_tag < 3.0, f"with-tag per-token stream took {with_tag:.1f}s — quadratic"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_streaming_partial_opener_tail_respects_code_context():
|
|
|
|
|
"""#3633 perf-fix follow-up (Codex): the bulk-skip fast path must not suppress
|
|
|
|
|
a trailing partial opener that sits inside code — only a partial opener in
|
|
|
|
|
PLAIN text is a forming block. Mirrors master parity for inline-backtick,
|
|
|
|
|
fenced, and indented code; a plain partial tail is still suppressed."""
|
|
|
|
|
ext = _extract_inline_thinking_from_content
|
|
|
|
|
# Inside code → the partial opener tail stays visible.
|
|
|
|
|
assert ext("answer `<thi", "", streaming=True)[0] == "answer `<thi"
|
|
|
|
|
assert ext("```\n<thi", "", streaming=True)[0] == "```\n<thi"
|
|
|
|
|
assert ext(" <thi", "", streaming=True)[0] == " <thi"
|
|
|
|
|
# Plain text → the forming partial opener is suppressed from display.
|
|
|
|
|
assert ext("answer <thi", "", streaming=True)[0] == "answer "
|
|
|
|
|
|
|
|
|
|
|
Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633) (#3853)
* fix(streaming): normalize inline thinking extraction across live and persisted turns (#3599)
# Conflicts:
# api/streaming.py
# static/messages.js
# static/ui.js
* fix(streaming): code-aware inline-thinking extraction + position-aware unclosed handling
Codex deep-review caught two regressions in the leading-only -> full-scan
rewrite (both silent data-mangling on the persist/reload path):
1. Code-span unawareness: the scanner only protected triple fences, so a
literal <think> in an inline single-backtick code span or an indented
(>=4-space/tab) code block got silently extracted into reasoning. Added
_inline_thinking_indented_code_at + inline-backtick tracking (Python +
the JS twin _thinkingIndentedCodeAt), so all three code contexts now keep
thinking tags visible.
2. Unclosed-tag truncation: any unmatched open tag moved the trailing prose
into reasoning. Now position-aware — a LEADING unclosed block (cut off
mid-thought) is still reasoning (#3455 intent), but an unclosed tag AFTER
visible content stays visible so literal typed tags don't truncate prose.
Gated partial handling on the previously-unused options.streaming param
(live streaming keeps 'still thinking' behavior; persist/reload does not).
Updated 2 tests that pinned the buggy behavior + added 4 regression tests
(inline-backtick, indented-code, mid-body-unclosed-visible, leading-unclosed-
extracted). Updated the node driver harness to include the new helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): recognize fenced code blocks indented 1-3 spaces
Codex round-3: a fence indented 1-3 spaces is valid Markdown but the fence
detector only matched at column 0, so a literal think tag inside such a fence
(not 4+-space indented code either) was still extracted. Both detectors
(_inline_thinking_fence_marker_at / _thinkingFenceMarkerAt) now walk back over
up to 3 leading spaces to a line start. Added backtick + tilde indented-fence
regression tests.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): O(n) inline-thinking scan + merge separate reasoning on reload
Round-4 Codex deep-review caught two real issues in my own fixes:
1. PERF (O(n^2)): the indented-code check (_inline_thinking_indented_code_at /
_thinkingIndentedCodeAt) scanned to line boundaries at EVERY character index,
plus the leading check sliced+stripped the whole prefix per unclosed tag. On
long no-newline content this was quadratic (~8.4s @ 200k, called repeatedly
on the streaming path). Replaced with incremental O(1)-per-iteration line
state (_line_is_indented_code / _lineIsIndentedCode evaluated only at line
starts) + a seen_nonspace flag. 200k now extracts in ~55-140ms.
2. RELOAD reasoning-drop: renderMessages() seeded the shared extractor with ''
so a message with BOTH an inline <think> block AND a separate m.reasoning
payload showed only the inline part — the separate payload was dropped
because the !thinkingText worklog resolution was then skipped. Now seeds with
the message's direct reasoning (m.reasoning_content||m.reasoning||...) so the
two MERGE (deduped); separate-only reasoning is preserved without promoting
it into visible prose.
Python + JS twins kept line-for-line parity. Added merge + perf + reload
regression tests; updated the reload structure test and the node driver harness
for the renamed helper.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): revert reload reasoning-seed; keep O(n) perf fix
Codex round-4 finding #2 (seed renderMessages' inline extractor with
m.reasoning so a separate payload merges) turned out to VIOLATE a deliberate
architectural invariant pinned by test_issue2565 +
test_sprint42: the reload content-extraction path must NOT touch
m.reasoning/m.reasoning_content — reasoning metadata is owned exclusively by
the Worklog Thinking Card path (_worklogReasoningTextFromMessage /
_assistantReasoningPayloadText), never conflated with inline-content
extraction (which would risk promoting provider reasoning into final-answer
prose). Reverted the ui.js seed to the PR's original `thinkingText` arg.
The inline+separate merge is still a genuine extractor capability (exercised
by the live streaming path via liveReasoningText) and is covered by a unit
test, just not invoked from the reload render path by design.
The O(n) perf fix (finding #1) and the code-awareness + position-aware
unclosed handling (rounds 1-3) are all retained.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): only lstrip extracted content when a leading block was removed
Codex round-5 catch: the extractor unconditionally lstripped the final content
(.lstrip() / .replace(/^\s+/,'')) even when NO thinking block was extracted, so
an assistant reply that legitimately starts with an indented code block or blank
lines lost its leading whitespace on live display, reload, and persistence. This
was a real regression vs master (master returned non-thinking content unchanged).
Now track leading_removed (set only when a LEADING thinking block/prefix is
actually extracted) and lstrip only in that case. Mid-body / no-thinking content
keeps its exact leading whitespace. Python + JS twins kept in parity; added
backend regression tests (indented-first preserved, leading-blank preserved,
leading-think still strips).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* fix(streaming): reconnect restore prefers raw inflight accumulator
Codex round-6 CORE catch: on reconnect, the single-live-message restore used
(_liveInflightAssistant.content || ''). Because the PR now splits a leading
unclosed <think> into empty content, restoring from the split content dropped
the open tag — so a later </think> token leaked into the visible reply and
corrupted the live accumulator. Restore from
(_fullInflightAssistant || _liveInflightAssistant.content || '') so the raw
open tag survives reconnect and the accumulator stays correct. Added a
reconnect-restore regression test.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633)
Unify inline-thinking (<think>/<|channel>/<|turn|>) extraction across live,
reload, and persisted turns (#3599/#3633, @rodboev). Deep-reviewed: Opus +
6 Codex rounds; maintainer fixes resolved every Codex finding — code-awareness
(inline-backtick/indented/1-3-space fences keep literal tags visible),
position-aware unclosed handling, O(n) line scanning (was O(n^2) on long
content), conditional lstrip (preserve leading whitespace when no leading block
removed), and a reconnect-restore CORE fix (raw accumulator preferred so an open
<think> tag survives reconnect). Python + JS twins in parity. Full suite 8330,
Opus SHIP-SAFE, Codex SAFE-TO-SHIP, ESLint/scope-undef/ruff clean.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
---------
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-09 00:28:05 +00:00
|
|
|
def test_timeout_wrapper_remains_out_of_scope():
|
|
|
|
|
assert "Request timed out. Please try again." in WORKSPACE_JS
|
|
|
|
|
assert "AbortController" in WORKSPACE_JS
|