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:
nesquena-hermes
2026-05-17 00:22:22 +00:00
parent 54f1a2acae
commit 8a950cfbdd
4 changed files with 174 additions and 6 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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