fix(tui): keep reasoning-only assistant turns visible on session resume

A thinking-only assistant turn (reasoning present, empty visible text) is
persisted with its reasoning fields and stays recallable from the transcript,
but `_history_to_messages` dropped it as "empty" before its reasoning was
attached. On desktop/TUI resume or reload the turn therefore vanished from the
session view while the agent could still recall it from a fresh session --
exactly the "messages disappear when the LLM uses its thinking block, but a new
session can recall them" symptom reported on #44022.

Keep an assistant turn when it carries reasoning, even with empty text, so the
desktop "Thinking…" disclosure has something to render. Genuinely empty turns
(no text, no reasoning, no tool calls) are still filtered out.

Refs #44022

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Adalsteinn Helgason 2026-06-11 17:22:07 +00:00 committed by Teknium
parent 643dc82793
commit 2667601c05
2 changed files with 53 additions and 7 deletions

View file

@ -847,6 +847,41 @@ def test_history_to_messages_preserves_tool_calls_for_resume_display():
]
def test_history_to_messages_keeps_reasoning_only_assistant_turn():
# A thinking-only assistant turn (reasoning present, no visible text) is
# persisted and recallable, but was dropped from the resumed session view
# as "empty" -- so it vanished while the agent could still recall it from
# the transcript. Keep it (with reasoning) so the desktop "Thinking…"
# disclosure renders. (#44022)
history = [
{"role": "user", "content": "think about this"},
{"role": "assistant", "content": "", "reasoning": "step-by-step thoughts"},
{"role": "assistant", "content": "here is the answer"},
]
assert server._history_to_messages(history) == [
{"role": "user", "text": "think about this"},
{"role": "assistant", "text": "", "reasoning": "step-by-step thoughts"},
{"role": "assistant", "text": "here is the answer"},
]
def test_history_to_messages_still_drops_empty_assistant_without_reasoning():
# A genuinely empty assistant turn (no text, no reasoning, no tool calls)
# remains filtered out -- the fix only spares reasoning-bearing turns.
history = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "", "reasoning": ""},
{"role": "assistant", "content": " "},
{"role": "assistant", "content": "real reply"},
]
assert server._history_to_messages(history) == [
{"role": "user", "text": "hi"},
{"role": "assistant", "text": "real reply"},
]
def test_history_to_messages_renders_multimodal_content():
# bb/gui preserves image URLs in the resume payload so the desktop
# renderer's extractEmbeddedImages can pull them back out and display

View file

@ -3708,16 +3708,27 @@ def _history_to_messages(history: list[dict]) -> list[dict]:
{"role": "tool", "name": name, "context": _tool_ctx(name, args)}
)
continue
if not content_text.strip():
# An assistant turn may carry only reasoning/thinking content with no
# visible text (extended-thinking turns, thinking-only recovery
# responses). Such a turn is persisted with its reasoning fields and is
# recallable from the transcript, but dropping it here as "empty" makes
# it vanish from the resumed/reloaded session view while the desktop's
# reasoning disclosure has nothing to render. Keep it when it carries
# reasoning so the "Thinking…" block still shows. (#44022)
reasoning_keys = (
"reasoning",
"reasoning_content",
"reasoning_details",
"codex_reasoning_items",
)
has_reasoning = role == "assistant" and any(
m.get(key) for key in reasoning_keys
)
if not content_text.strip() and not has_reasoning:
continue
msg = {"role": role, "text": content_text}
if role == "assistant":
for key in (
"reasoning",
"reasoning_content",
"reasoning_details",
"codex_reasoning_items",
):
for key in reasoning_keys:
if key in m and m.get(key) is not None:
msg[key] = m.get(key)
messages.append(msg)