fix: deduplicate workspace-prefixed user turns

This commit is contained in:
ai-ag2026
2026-05-08 12:14:13 +02:00
committed by nesquena-hermes
parent 773857d159
commit f6d09e06ca
2 changed files with 47 additions and 2 deletions
+17 -2
View File
@@ -586,6 +586,11 @@ def _message_text(value) -> str:
return _strip_thinking_markup(str(value or '').strip())
def _strip_workspace_prefix(text: str) -> str:
"""Remove WebUI's model-facing workspace tag from display identity text."""
return re.sub(r'^\s*\[Workspace:[^\]]+\]\s*', '', str(text or '')).strip()
def _first_exchange_snippets(messages):
"""Return (first_user_text, first_assistant_text) snippets for title generation.
@@ -1433,6 +1438,12 @@ def _message_identity(msg):
role = str(msg.get('role') or '')
content = msg.get('content', '')
text = _message_text(content)
if role == 'user':
# WebUI sends the model a workspace-prefixed user_message while the
# visible optimistic bubble contains only the human text. Treat them as
# the same turn for merge/dedup purposes; otherwise compaction results
# render two adjacent user bubbles ("Ok" and "[Workspace...]\nOk").
text = _strip_workspace_prefix(text)
if not text and not msg.get('tool_call_id') and not msg.get('tool_calls'):
return None
return (
@@ -1471,7 +1482,7 @@ def _find_current_user_turn(messages, msg_text):
if not isinstance(msg, dict) or msg.get('role') != 'user':
continue
fallback = idx
text = " ".join(_message_text(msg.get('content', '')).split())
text = " ".join(_strip_workspace_prefix(_message_text(msg.get('content', ''))).split())
if needle and (needle in text or text in needle):
return idx
return fallback
@@ -1558,7 +1569,11 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex
continue
if _is_context_compression_marker(msg) and key is not None and key in seen:
continue
merged.append(copy.deepcopy(msg))
display_msg = msg
if key is not None and key == current_user_key and isinstance(msg, dict) and msg.get('role') == 'user':
display_msg = copy.deepcopy(msg)
display_msg['content'] = msg_text
merged.append(copy.deepcopy(display_msg))
if key is not None:
seen.add(key)
return merged
@@ -47,6 +47,36 @@ def test_session_persists_model_context_separately_from_display_transcript(tmp_p
assert _sanitize_messages_for_api(_session_context_messages(reloaded)) == compacted_context
def test_workspace_prefixed_current_user_after_compaction_is_not_duplicated():
previous_display = [
{"role": "user", "content": "older prompt"},
{"role": "assistant", "content": "older answer"},
]
previous_context = list(previous_display)
compacted_result = [
{
"role": "assistant",
"content": "[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.",
},
{"role": "user", "content": "[Workspace: /home/manfred/.hermes/workspace]\nOk, mache weiter"},
{"role": "assistant", "content": "continuing"},
]
merged = _merge_display_messages_after_agent_result(
previous_display,
previous_context,
compacted_result,
"Ok, mache weiter",
)
assert [m["role"] for m in merged] == ["user", "assistant", "assistant", "user", "assistant"]
assert [m["content"] for m in merged[-2:]] == [
"Ok, mache weiter",
"continuing",
]
assert sum(1 for m in merged if m.get("role") == "user" and "Ok, mache weiter" in m.get("content", "")) == 1
def test_compacted_agent_result_keeps_old_prompts_and_appends_current_turn():
previous_display = [
{"role": "user", "content": "first prompt that must remain visible"},