From f69a81c8c327bc522548c338b726e5338898902d Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Thu, 7 May 2026 23:57:01 +0200 Subject: [PATCH] fix: preserve pending turn during stale cleanup --- api/routes.py | 8 +++++- tests/test_issue1361_cancel_data_loss.py | 34 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/api/routes.py b/api/routes.py index a8f9bd8e..73d5201c 100644 --- a/api/routes.py +++ b/api/routes.py @@ -675,6 +675,7 @@ def _clear_stale_stream_state(session) -> bool: with _get_session_agent_lock(session.session_id): if getattr(session, "active_stream_id", None) != stream_id: return False + _materialize_pending_user_turn_before_error(session) session.active_stream_id = None if hasattr(session, "pending_user_message"): session.pending_user_message = None @@ -1508,7 +1509,12 @@ from api.workspace import ( _workspace_blocked_roots, ) from api.upload import handle_upload, handle_upload_extract, handle_transcribe -from api.streaming import _sse, _run_agent_streaming, cancel_stream +from api.streaming import ( + _sse, + _run_agent_streaming, + cancel_stream, + _materialize_pending_user_turn_before_error, +) from api.providers import get_providers, get_provider_quota, set_provider_key, remove_provider_key from api.onboarding import ( apply_onboarding_setup, diff --git a/tests/test_issue1361_cancel_data_loss.py b/tests/test_issue1361_cancel_data_loss.py index 3e19291a..09fc77f0 100644 --- a/tests/test_issue1361_cancel_data_loss.py +++ b/tests/test_issue1361_cancel_data_loss.py @@ -366,6 +366,40 @@ def test_stream_error_pending_materialization_does_not_duplicate_eager_checkpoin assert [m.get("role") for m in s.messages].count("user") == 1 +def test_stale_stream_cleanup_materializes_pending_turn_before_clearing_state(): + """A zombie/stale stream repair must preserve the pending user prompt. + + If the process dies after chat_start saved pending_user_message but before the + agent merges the user turn, /api/session stale cleanup must not clear that + pending field without first appending a durable user message. + """ + from api.routes import _clear_stale_stream_state + + sid = "test_pending_error_d3_stale" + s = _make_session( + session_id=sid, + pending_msg="please make the GUI fully usable", + messages=[{"role": "assistant", "content": "previous answer"}], + ) + s.pending_started_at = 1778187755.0 + s.pending_attachments = [{"name": "visible-state.png"}] + # No matching STREAMS entry: this simulates a dead worker/server restart. + + cleared = _clear_stale_stream_state(s) + + assert cleared is True + assert s.active_stream_id is None + assert s.pending_user_message is None + assert s.messages[-1]["role"] == "user" + assert s.messages[-1]["content"] == "please make the GUI fully usable" + assert s.messages[-1]["timestamp"] == 1778187755 + assert s.messages[-1]["attachments"] == [{"name": "visible-state.png"}] + + reloaded = models.get_session(sid, metadata_only=False) + assert reloaded.messages[-1]["role"] == "user" + assert reloaded.messages[-1]["content"] == "please make the GUI fully usable" + + # ── Structural guard: pin call sites of the materialize helper at error branches ── def test_materialize_helper_called_immediately_before_error_path_clears():