diff --git a/codex-rs/core/src/next_prompt_suggestion.rs b/codex-rs/core/src/next_prompt_suggestion.rs index a857ea1615..9b19ca14e3 100644 --- a/codex-rs/core/src/next_prompt_suggestion.rs +++ b/codex-rs/core/src/next_prompt_suggestion.rs @@ -258,6 +258,10 @@ async fn session_is_idle_for_suggestion(sess: &Session) -> bool { tracing::debug!("next prompt suggestion skipped while a turn is active"); return false; } + if sess.conversation.running_state().await.is_some() { + tracing::debug!("next prompt suggestion skipped while realtime conversation is active"); + return false; + } true } diff --git a/codex-rs/core/tests/suite/next_prompt_suggestion.rs b/codex-rs/core/tests/suite/next_prompt_suggestion.rs index 9d96024f44..9f4bf15349 100644 --- a/codex-rs/core/tests/suite/next_prompt_suggestion.rs +++ b/codex-rs/core/tests/suite/next_prompt_suggestion.rs @@ -1,11 +1,15 @@ use anyhow::Result; +use codex_protocol::protocol::ConversationStartParams; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::RealtimeOutputModality; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; +use core_test_support::responses::start_websocket_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; @@ -122,6 +126,62 @@ async fn suggest_next_prompt_skips_early_history_without_request() -> Result<()> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn suggest_next_prompt_skips_active_realtime_conversation_without_request() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "first turn complete"), + ev_completed("resp-1"), + ]), + sse(vec![ + ev_response_created("resp-2"), + ev_assistant_message("msg-2", "second turn complete"), + ev_completed("resp-2"), + ]), + ], + ) + .await; + let realtime_server = start_websocket_server(vec![vec![vec![]]]).await; + let realtime_base_url = realtime_server.uri().to_string(); + let mut builder = test_codex().with_config(move |config| { + config.experimental_realtime_ws_base_url = Some(realtime_base_url); + config.experimental_realtime_ws_startup_context = Some(String::new()); + }); + let test = builder.build(&server).await?; + test.submit_turn("first task").await?; + test.submit_turn("second task").await?; + test.codex + .submit(Op::RealtimeConversationStart(ConversationStartParams { + output_modality: RealtimeOutputModality::Audio, + prompt: Some(Some("backend prompt".to_string())), + realtime_session_id: None, + transport: None, + voice: None, + })) + .await?; + wait_for_event(&test.codex, |msg| { + matches!(msg, EventMsg::RealtimeConversationStarted(_)) + }) + .await; + + let suggestion = test + .codex + .suggest_next_prompt(CancellationToken::new()) + .await?; + assert_eq!(suggestion, None); + assert_eq!(responses.requests().len(), 2); + + test.codex.submit(Op::RealtimeConversationClose).await?; + realtime_server.shutdown().await; + Ok(()) +} + fn ev_rate_limits() -> serde_json::Value { json!({ "type": "codex.rate_limits",