diff --git a/api/gateway_chat.py b/api/gateway_chat.py index 548bd60e..f2bf9e8c 100644 --- a/api/gateway_chat.py +++ b/api/gateway_chat.py @@ -826,6 +826,12 @@ def _run_gateway_chat_streaming( s = get_session(session_id) if not _stream_writeback_is_current(s, stream_id): return + # A late Stop can land after Gateway has yielded a full answer but + # before success writeback. Treat it as cancellation so any + # credential-exhausted process-wakeup pause stays in place. + if cancel_event.is_set(): + put_gateway_event("cancel", {"message": "Cancelled by user"}) + return now = time.time() # Preserve subsecond ordering for gateway-backed turns. Using an # integer seconds timestamp gives the user and assistant rows the diff --git a/tests/test_issue3929_process_wakeup_pause.py b/tests/test_issue3929_process_wakeup_pause.py index 3c2c8551..32bcc911 100644 --- a/tests/test_issue3929_process_wakeup_pause.py +++ b/tests/test_issue3929_process_wakeup_pause.py @@ -375,13 +375,16 @@ def test_success_path_clears_process_wakeup_pause_after_late_cancel_checks(): assert state_sync_idx < final_cancel_idx < pause_clear_idx < done_payload_idx -def test_gateway_success_path_clears_process_wakeup_pause_before_save(): +def test_gateway_success_path_checks_cancel_before_clearing_process_wakeup_pause(): src = Path(__file__).parent.parent.joinpath("api", "gateway_chat.py").read_text(encoding="utf-8") + current_idx = src.index("if not _stream_writeback_is_current(s, stream_id):") + final_cancel_idx = src.index("if cancel_event.is_set():", current_idx) pending_clear_idx = src.index("s.pending_user_source = None") pause_clear_idx = src.index('clear_process_wakeup_pause(s, reason="run_completed")') save_idx = src.index("s.save()", pause_clear_idx) - assert pending_clear_idx < pause_clear_idx < save_idx + assert current_idx < final_cancel_idx < pause_clear_idx < save_idx + assert pending_clear_idx < pause_clear_idx def test_process_wakeup_pause_does_not_suppress_explicit_non_wakeup_turn(tmp_path, monkeypatch):