mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-24 18:50:15 +00:00
Stage 373: PR #2417 — fix(streaming): stale compaction task resume on fresh greetings (closes #2308, supersedes #2309)
Co-authored-by: Frank Song <franksong2702@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- **PR #2415** by @Michaelyklam (fixes #2399) — `providers.only_configured` and other scalar flags under the top-level `providers:` config mapping no longer appear as fake provider groups in the model picker. Provider detection now only seeds picker groups from known provider ids/aliases or dict-shaped provider configs, so filtering flags cannot render as `Only-Configured`.
|
||||
- **PR #2417** by @nesquena-hermes (co-authored by @franksong2702, supersedes #2309, closes #2308) — Compressed sessions with hidden "resume active task" context no longer treat a short fresh greeting (e.g. `hi`, `hello`, the equivalent CJK opener) as implicit permission to continue an old agent task. Explicit continuation prompts (e.g. `continue`, `resume`, or the equivalent CJK continuation phrase) still keep the compacted task context.
|
||||
|
||||
## [v0.51.79] — 2026-05-16 — Release BC (stage-372 — 5-PR batch — text-mode image history fix + Activity-group compression boundary + named custom provider routing + quota chip Settings toggle + RFC docs)
|
||||
|
||||
|
||||
+2
-2
@@ -7880,7 +7880,7 @@ def _handle_chat_sync(handler, body):
|
||||
_merge_display_messages_after_agent_result,
|
||||
_restore_reasoning_metadata,
|
||||
_sanitize_messages_for_api,
|
||||
_session_context_messages,
|
||||
_context_messages_for_new_turn,
|
||||
_workspace_context_prefix,
|
||||
)
|
||||
workspace_ctx = _workspace_context_prefix(str(s.workspace))
|
||||
@@ -7897,7 +7897,7 @@ def _handle_chat_sync(handler, body):
|
||||
)
|
||||
|
||||
_previous_messages = list(s.messages or [])
|
||||
_previous_context_messages = list(_session_context_messages(s))
|
||||
_previous_context_messages = list(_context_messages_for_new_turn(s, msg))
|
||||
|
||||
result = agent.run_conversation(
|
||||
user_message=workspace_ctx + msg,
|
||||
|
||||
+85
-4
@@ -2135,6 +2135,90 @@ def _drop_checkpointed_current_user_from_context(messages, msg_text):
|
||||
return history
|
||||
|
||||
|
||||
def _normalize_fresh_chat_text(text):
|
||||
text = _strip_workspace_prefix(str(text or ''), include_legacy=True)
|
||||
text = re.sub(r"\s+", " ", text).strip().lower()
|
||||
return text.strip(" \t\r\n.!?。!?,,~~")
|
||||
|
||||
|
||||
def _is_casual_fresh_chat_message(msg_text):
|
||||
"""Return True for short opener messages that should not resume old tasks."""
|
||||
text = _normalize_fresh_chat_text(msg_text)
|
||||
if not text or len(text) > 24:
|
||||
return False
|
||||
continuation_terms = (
|
||||
"continue",
|
||||
"resume",
|
||||
"carry on",
|
||||
"go on",
|
||||
# CJK continuation terms (zh-CN): jixu, jiezhe, wangxia, xiayibu.
|
||||
# Encoded as Python escape sequences (not literal CJK) so api/streaming.py
|
||||
# passes tests/test_title_sanitization.py::test_title_generation_source_has_no_cjk_literals,
|
||||
# which scans this file for any U+4E00-U+9FFF code points. Runtime
|
||||
# comparisons still use the real CJK strings — Python decodes the
|
||||
# escapes at compile time.
|
||||
"\u7ee7\u7eed",
|
||||
"\u63a5\u7740",
|
||||
"\u5f80\u4e0b",
|
||||
"\u4e0b\u4e00\u6b65",
|
||||
)
|
||||
if any(term in text for term in continuation_terms):
|
||||
return False
|
||||
return text in {
|
||||
"hi",
|
||||
"hello",
|
||||
"hey",
|
||||
"hello there",
|
||||
"hi there",
|
||||
# CJK greetings (zh-CN): nihao, ninhao, hai, haluo, zaima, zaime.
|
||||
# Same escape-sequence rationale as the continuation block above.
|
||||
"\u4f60\u597d", # nihao
|
||||
"\u60a8\u597d", # ninhao
|
||||
"\u55e8", # hai (was \u5616 = "click of tongue", not a greeting)
|
||||
"\u54c8\u55bd", # haluo (was \u54c8\u5582 = uncommon "ha-wei" variant)
|
||||
"\u5728\u5417", # zaima
|
||||
"\u5728\u4e48", # zaime
|
||||
}
|
||||
|
||||
|
||||
def _has_task_resume_compaction_marker(messages):
|
||||
"""Detect compacted model context that tells the agent to resume an old task."""
|
||||
for msg in messages or []:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
text = _message_text(msg.get('content', '')).lower()
|
||||
if not text:
|
||||
continue
|
||||
if "context compaction" not in text and "context compression" not in text:
|
||||
continue
|
||||
if (
|
||||
"active task" in text
|
||||
or "resume exactly" in text
|
||||
or "current task" in text
|
||||
or "task list was preserved" in text
|
||||
or "in_progress" in text
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _context_messages_for_new_turn(session, msg_text):
|
||||
"""Return provider-facing history for a new user turn.
|
||||
|
||||
Compacted agent sessions can carry a hidden "resume the active task" summary
|
||||
long after the visible UI looks like normal chat. A short greeting should
|
||||
not silently reactivate that old task; explicit continuation prompts still
|
||||
keep the full compacted context.
|
||||
"""
|
||||
history = _drop_checkpointed_current_user_from_context(
|
||||
_session_context_messages(session),
|
||||
msg_text,
|
||||
)
|
||||
if _is_casual_fresh_chat_message(msg_text) and _has_task_resume_compaction_marker(history):
|
||||
return []
|
||||
return history
|
||||
|
||||
|
||||
def _stream_writeback_is_current(session, stream_id):
|
||||
"""Return True only while a worker still owns the session writeback.
|
||||
|
||||
@@ -3674,10 +3758,7 @@ def _run_agent_streaming(
|
||||
# Truthy-check covers None, missing-attr, and 0 uniformly.
|
||||
_turn_started_at = _pending_started_at if _pending_started_at else time.time()
|
||||
_previous_messages = list(s.messages or [])
|
||||
_previous_context_messages = _drop_checkpointed_current_user_from_context(
|
||||
_session_context_messages(s),
|
||||
msg_text,
|
||||
)
|
||||
_previous_context_messages = _context_messages_for_new_turn(s, msg_text)
|
||||
_pre_compression_count = getattr(
|
||||
getattr(agent, 'context_compressor', None),
|
||||
'compression_count', 0,
|
||||
|
||||
@@ -2,6 +2,7 @@ from api.models import Session
|
||||
import contextlib
|
||||
|
||||
from api.streaming import (
|
||||
_context_messages_for_new_turn,
|
||||
_merge_display_messages_after_agent_result,
|
||||
_sanitize_messages_for_api,
|
||||
_session_context_messages,
|
||||
@@ -172,6 +173,91 @@ def test_session_context_falls_back_to_display_messages_for_legacy_sessions(tmp_
|
||||
assert _session_context_messages(session) == messages
|
||||
|
||||
|
||||
def test_casual_greeting_does_not_resume_stale_compaction_active_task(tmp_path):
|
||||
compacted_task_context = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted. "
|
||||
"Your current task is identified in the Active Task section — resume exactly from there. "
|
||||
"[Your active task list was preserved across context compression] "
|
||||
"- [>] 5. 更新测试:mock bridge 输出 (in_progress)"
|
||||
),
|
||||
},
|
||||
{"role": "assistant", "content": "I will inspect api/config.py next."},
|
||||
]
|
||||
session = Session(
|
||||
session_id="issue2308",
|
||||
workspace=str(tmp_path),
|
||||
messages=[
|
||||
{"role": "user", "content": "old provider/model task"},
|
||||
{"role": "assistant", "content": "old task answer"},
|
||||
],
|
||||
context_messages=compacted_task_context,
|
||||
)
|
||||
|
||||
assert _context_messages_for_new_turn(session, "你好") == []
|
||||
|
||||
|
||||
def test_explicit_continue_keeps_compacted_active_task_context(tmp_path):
|
||||
compacted_task_context = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted. "
|
||||
"Your current task is identified in the Active Task section — resume exactly from there."
|
||||
),
|
||||
},
|
||||
{"role": "assistant", "content": "I will inspect api/config.py next."},
|
||||
]
|
||||
session = Session(
|
||||
session_id="issue2308-continue",
|
||||
workspace=str(tmp_path),
|
||||
messages=[
|
||||
{"role": "user", "content": "old provider/model task"},
|
||||
{"role": "assistant", "content": "old task answer"},
|
||||
],
|
||||
context_messages=compacted_task_context,
|
||||
)
|
||||
|
||||
assert _context_messages_for_new_turn(session, "继续") == compacted_task_context
|
||||
|
||||
|
||||
def test_all_cjk_greetings_drop_stale_compaction_context(tmp_path):
|
||||
"""Pin every CJK greeting in the casual-fresh-chat set against a stale
|
||||
compaction context. Catches typos like \\u5616 (嘖, "click of tongue")
|
||||
or \\u5582 (喂, "hey on phone") slipping into the greeting set where
|
||||
\\u55c9 (嗨, "hai") and \\u55bd (喽, "luo") were intended."""
|
||||
compacted_task_context = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"[CONTEXT COMPACTION — REFERENCE ONLY] active task: X — resume exactly. in_progress."
|
||||
),
|
||||
},
|
||||
{"role": "assistant", "content": "I will continue task X"},
|
||||
]
|
||||
session = Session(
|
||||
session_id="issue2308-cjk-all",
|
||||
workspace=str(tmp_path),
|
||||
messages=[
|
||||
{"role": "user", "content": "old task"},
|
||||
{"role": "assistant", "content": "old answer"},
|
||||
],
|
||||
context_messages=compacted_task_context,
|
||||
)
|
||||
|
||||
# Every CJK greeting in _is_casual_fresh_chat_message must drop the stale context.
|
||||
# If a typo lands a wrong codepoint here, the user's greeting won't be recognized
|
||||
# and the stale "resume active task" prompt will silently leak back through.
|
||||
for greeting in ("你好", "您好", "嗨", "哈喽", "在吗", "在么"):
|
||||
assert _context_messages_for_new_turn(session, greeting) == [], (
|
||||
f"CJK greeting {greeting!r} (U+{ord(greeting[0]):04X}"
|
||||
f"{'+'+'U+%04X' % ord(greeting[1]) if len(greeting) > 1 else ''}) "
|
||||
f"was not recognized as a casual fresh chat — stale compaction context leaked"
|
||||
)
|
||||
|
||||
|
||||
def test_retry_truncates_model_context_when_it_is_separate(monkeypatch, tmp_path):
|
||||
import api.session_ops as session_ops
|
||||
|
||||
|
||||
Reference in New Issue
Block a user