From 0872800a88ae60cf82ff465bc943ce49974728c0 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Wed, 8 Jul 2026 07:49:04 +0000 Subject: [PATCH] fix(chat): keep live reasoning on one renderer path Clean rebase of rodboev's #5773 (rebase-first). Co-authored-by: rodboev --- static/messages.js | 6 ++-- tests/test_regressions.py | 8 +++-- ...t_stable_assistant_turn_anchor_registry.py | 4 ++- tests/test_ui_tool_call_cleanup.py | 34 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/static/messages.js b/static/messages.js index 1c3749dd1..efc721aea 100644 --- a/static/messages.js +++ b/static/messages.js @@ -5024,8 +5024,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ if(d.text&&S.session&&S.session.session_id===activeSid) _completeAutomaticCompressionOnLiveProgress(activeSid); syncInflightAssistantMessage(); if(text&&S.session&&S.session.session_id===activeSid){ - _upsertAnchorReasoning(_liveThinkingText()); - _updateLiveThinkingCard(_liveThinkingText()); + const liveThinkingText=_liveThinkingText(); + if(!_upsertAnchorReasoning(liveThinkingText)){ + _updateLiveThinkingCard(liveThinkingText); + } } }); diff --git a/tests/test_regressions.py b/tests/test_regressions.py index b1c1aff37..b0dd5b426 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -931,8 +931,12 @@ def test_messages_js_supports_live_reasoning_and_tool_completion(cleanup_test_se "messages.js must listen for live reasoning SSE events" assert "liveReasoningText += text" in src, \ "live reasoning SSE events must update the active Worklog Thinking Card text" - assert "_updateLiveThinkingCard(_liveThinkingText())" in src, \ - "live reasoning SSE events must refresh the current segment's Worklog Thinking Card" + assert "const liveThinkingText=_liveThinkingText();" in src, \ + "live reasoning SSE events must compute the current segment's Worklog Thinking Card text once" + assert "if(!_upsertAnchorReasoning(liveThinkingText))" in src, \ + "live reasoning SSE events must prefer the anchor renderer before falling back" + assert "_updateLiveThinkingCard(liveThinkingText)" in src, \ + "live reasoning SSE events must keep the current segment's Worklog Thinking Card as fallback" assert "source.addEventListener('tool_complete'" in src or 'source.addEventListener("tool_complete"' in src, \ "messages.js must listen for live tool completion SSE events" assert "function _parseStreamState()" in src, \ diff --git a/tests/test_stable_assistant_turn_anchor_registry.py b/tests/test_stable_assistant_turn_anchor_registry.py index 58fd5d871..feb6d9a52 100644 --- a/tests/test_stable_assistant_turn_anchor_registry.py +++ b/tests/test_stable_assistant_turn_anchor_registry.py @@ -1306,7 +1306,9 @@ def test_slice6_live_shadow_feed_wires_anchor_scene_for_visible_order_handoff(): assert "_upsertAnchorProcessProse(displayText" in src reasoning_body = _event_listener_body(src, "reasoning") assert "_applyToAnchor" not in reasoning_body - assert "_upsertAnchorReasoning(_liveThinkingText())" in reasoning_body + assert "const liveThinkingText=_liveThinkingText();" in reasoning_body + assert "if(!_upsertAnchorReasoning(liveThinkingText))" in reasoning_body + assert "_updateLiveThinkingCard(liveThinkingText)" in reasoning_body assert "function _flushReasoningToAnchor()" in src assert "_upsertAnchorReasoning(reasoningText" in src assert "`live-reasoning:${streamId}:final`" in src diff --git a/tests/test_ui_tool_call_cleanup.py b/tests/test_ui_tool_call_cleanup.py index 4e6dea614..b69d8cef1 100644 --- a/tests/test_ui_tool_call_cleanup.py +++ b/tests/test_ui_tool_call_cleanup.py @@ -670,6 +670,40 @@ class TestToolCallGroupingStatic: "Tool starts must not split consecutive tools into one-tool Activity rows." ) + def test_reasoning_stream_uses_one_live_renderer_path(self): + reasoning_match = re.search( + r"source\.addEventListener\('reasoning',e=>\{(.*?)\n\s*\}\);", + MESSAGES_JS, + re.S, + ) + assert reasoning_match, "reasoning listener not found" + reasoning_fn = reasoning_match.group(1) + render_live_thinking_fn = _function_body(MESSAGES_JS, "_renderLiveThinking") + + assert reasoning_fn.count("_liveThinkingText()") == 1, ( + "_liveThinkingText() should be computed once inside the active-session branch." + ) + assert "const liveThinkingText=_liveThinkingText();" in reasoning_fn, ( + "Reasoning SSE updates should cache the live thinking text before routing." + ) + assert "_upsertAnchorReasoning(liveThinkingText)" in reasoning_fn, ( + "Anchor reasoning must remain the primary renderer path." + ) + assert reasoning_fn.index("_upsertAnchorReasoning(liveThinkingText)") < reasoning_fn.index( + "_updateLiveThinkingCard(liveThinkingText)" + ), ( + "The legacy thinking card should only run after anchor upsert fails." + ) + assert "if(!_upsertAnchorReasoning(liveThinkingText)){" in reasoning_fn, ( + "The legacy thinking card should be a falsy-anchor fallback." + ) + assert reasoning_fn.count("_updateLiveThinkingCard(liveThinkingText)") == 1, ( + "Reasoning SSE updates should call the live thinking card only in fallback." + ) + assert "_updateLiveThinkingCard(" in render_live_thinking_fn, ( + "Inline parsed thinking still needs the live thinking card renderer." + ) + def test_live_thinking_card_is_segment_scoped_not_global_singleton(self): live_thinking_fn = _function_body(UI_JS, "appendThinking") placement_fn = _function_body(MESSAGES_JS, "_liveThinkingPlacement")