fix(bridge): block thinking spinner kaomoji from contaminating conversation history (#1051)

The hermes-agent CLI KawaiiSpinner sends decorative kaomoji text
like "(◕‿◕✿) pondering..." through thinking_callback for its TUI
widget.  The bridge forwarded this as thinking.delta events, which
the frontend stored in the message reasoning field.

Over long conversations this contaminated the model's context:
_copy_reasoning_content_for_api promoted the kaomoji text to
reasoning_content, causing the LLM to reproduce kaomoji patterns
in a self-reinforcing degradation loop.

Fix: _make_thinking_callback unconditionally sends empty text.
thinking_callback is purely CLI spinner status — it has no place in
conversation history.  Actual model reasoning (reasoning.delta) is
unaffected.
This commit is contained in:
GoldenFishX
2026-05-27 09:42:04 +08:00
committed by GitHub
parent 42f7b64ffb
commit 3cede6fb7f
@@ -740,7 +740,7 @@ class AgentPool:
session_db=self._db.get_for_profile(profile),
ephemeral_system_prompt=prompt,
status_callback=self._status_callback(session_id),
thinking_callback=self._text_event_callback(session_id, "thinking.delta"),
thinking_callback=self._make_thinking_callback(session_id),
reasoning_callback=self._text_event_callback(session_id, "reasoning.delta"),
tool_progress_callback=self._tool_progress_callback(session_id),
tool_start_callback=self._tool_start_callback(session_id),
@@ -992,6 +992,28 @@ class AgentPool:
return callback
def _make_thinking_callback(self, session_id: str):
"""Create a thinking callback that never forwards spinner text as content.
The hermes-agent CLI uses thinking_callback for its KawaiiSpinner TUI
widget — sending decorative text like "(◕‿◕✿) pondering..." during
API calls. This is pure CLI UX decoration; it has no place in Web UI
conversation history.
Prior behaviour forwarded this text as thinking.delta events, which the
frontend stored in the message reasoning field. Over long conversations
this contaminated the model's context: the LLM learned to reproduce
kaomoji patterns, creating a self-reinforcing degradation loop.
This callback sends empty text unconditionally. The model's real
reasoning content arrives through reasoning_callback → reasoning.delta,
which is unaffected.
"""
def callback(text=None):
self._append_event(session_id, {"event": "thinking.delta", "text": ""})
return callback
def _tool_start_callback(self, session_id: str):
def callback(tool_call_id, function_name, function_args):
self._append_event(session_id, {