diff --git a/api/streaming.py b/api/streaming.py index a509261c..8556cda0 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -2332,21 +2332,22 @@ def _has_task_resume_compaction_marker(messages): return False +def _new_turn_context_from_messages(messages, msg_text): + """Return provider-facing history for a new user turn from a message list.""" + history = _drop_checkpointed_current_user_from_context(messages, msg_text) + if _is_casual_fresh_chat_message(msg_text) and _has_task_resume_compaction_marker(history): + return [] + return history + + 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. + in context_messages. If the user starts a fresh casual greeting in that old + session, do not feed that stale active-task summary back to the model. """ - 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 + return _new_turn_context_from_messages(_session_context_messages(session), msg_text) def _stream_writeback_is_current(session, stream_id): @@ -3965,7 +3966,7 @@ def _run_agent_streaming( state_messages=_external_state_messages, ) or [] ) - _previous_context_messages = _drop_checkpointed_current_user_from_context( + _previous_context_messages = _new_turn_context_from_messages( reconciled_state_db_messages_for_session( s, prefer_context=True, diff --git a/tests/test_issue1217_transcript_compaction.py b/tests/test_issue1217_transcript_compaction.py index 711d4131..c7ebe4bd 100644 --- a/tests/test_issue1217_transcript_compaction.py +++ b/tests/test_issue1217_transcript_compaction.py @@ -1,10 +1,12 @@ -from api.models import Session +from api.models import Session, reconciled_state_db_messages_for_session import contextlib +from types import SimpleNamespace from api.streaming import ( _assistant_reply_added_after_current_turn, _context_messages_for_new_turn, _merge_display_messages_after_agent_result, + _new_turn_context_from_messages, _sanitize_messages_for_api, _session_context_messages, ) @@ -314,6 +316,40 @@ def test_explicit_continue_keeps_compacted_active_task_context(tmp_path): assert _context_messages_for_new_turn(session, "继续") == compacted_task_context +def test_streaming_reconciled_context_keeps_casual_greeting_suppression(): + 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." + ), + "timestamp": 1.0, + }, + {"role": "assistant", "content": "I will inspect api/config.py next.", "timestamp": 2.0}, + ] + session = SimpleNamespace( + session_id="issue2308-streaming", + messages=[{"role": "user", "content": "old task", "timestamp": 0.5}], + context_messages=compacted_task_context, + ) + external_state_messages = list(compacted_task_context) + + # Mirror the streaming pre-turn assembly for prefer_context=True: reconcile + # sidecar context with one state.db snapshot, then apply the normal new-turn + # context filter that suppresses casual greetings from resuming stale tasks. + previous_context_messages = _new_turn_context_from_messages( + reconciled_state_db_messages_for_session( + session, + prefer_context=True, + state_messages=external_state_messages, + ), + "你好", + ) + + assert previous_context_messages == [] + + 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")