Stop mirroring Codex user input into realtime (#27116)

## Why

The realtime frontend model and the backing Codex thread should present
one coherent assistant. Raw typed messages, steers, and worker reports
belong to the orchestrator; the frontend model should receive the
orchestrator's user-facing result rather than a second copy of those
inputs.

Today normal `turn/start` input is automatically inserted into the
realtime conversation, while `turn/steer` is not. Besides creating
inconsistent context, this can make the frontend model react
independently before Codex has produced the response it should speak.

## What changed

- Remove automatic accepted-user-input mirroring into realtime
- Remove the mirror-only echo-suppression flag and dead V2 prefix helper
- Preserve explicit app-to-realtime text injection and FEM-to-Codex
delegation
- Replace the positive mirror tests and obsolete snapshots with a
negative routing regression test

## Test plan

- `cargo test -p codex-core
conversation_user_text_turn_is_not_sent_to_realtime`
- `cargo test -p codex-core
conversation_startup_context_is_truncated_and_sent_once_per_start`
- `cargo test -p codex-core inbound_handoff_request_starts_turn`
This commit is contained in:
guinness-oai
2026-06-09 15:20:01 -07:00
committed by GitHub
Unverified
parent f2969f36e8
commit cc8325f181
6 changed files with 13 additions and 252 deletions
@@ -739,10 +739,6 @@ fn prefix_realtime_text(text: String, prefix: &str, session_kind: RealtimeSessio
format!("{prefix}{text}")
}
pub(crate) fn prefix_realtime_v2_text(text: String, prefix: &str) -> String {
prefix_realtime_text(text, prefix, RealtimeSessionKind::V2)
}
fn validate_realtime_voice(version: RealtimeWsVersion, voice: RealtimeVoice) -> CodexResult<()> {
let voices = RealtimeVoicesList::builtin();
let allowed = match version {
+2 -45
View File
@@ -15,10 +15,6 @@ use crate::session::session::Session;
use crate::session::session::SessionSettingsUpdate;
use crate::config::Config;
use crate::realtime_context::REALTIME_TURN_TOKEN_BUDGET;
use crate::realtime_context::truncate_realtime_text_to_token_budget;
use crate::realtime_conversation::REALTIME_USER_TEXT_PREFIX;
use crate::realtime_conversation::prefix_realtime_v2_text;
use crate::review_prompts::resolve_review_request;
use crate::session::spawn_review_thread;
use crate::tasks::CompactTask;
@@ -54,9 +50,7 @@ use codex_protocol::request_user_input::RequestUserInputResponse;
use crate::context_manager::is_user_turn_boundary;
use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::items::UserMessageItem;
use codex_protocol::mcp::RequestId as ProtocolRequestId;
use codex_protocol::user_input::UserInput;
use codex_rmcp_client::ElicitationAction;
use codex_rmcp_client::ElicitationResponse;
use serde_json::Value;
@@ -91,14 +85,7 @@ pub async fn user_input_or_turn(
op: Op,
client_user_message_id: Option<String>,
) {
user_input_or_turn_inner(
sess,
sub_id,
op,
/*mirror_user_text_to_realtime*/ Some(()),
client_user_message_id,
)
.await;
user_input_or_turn_inner(sess, sub_id, op, client_user_message_id).await;
}
pub async fn update_thread_settings(
@@ -196,7 +183,6 @@ pub(super) async fn user_input_or_turn_inner(
sess: &Arc<Session>,
sub_id: String,
op: Op,
mirror_user_text_to_realtime: Option<()>,
client_user_message_id: Option<String>,
) {
let Op::UserInput {
@@ -230,7 +216,7 @@ pub(super) async fn user_input_or_turn_inner(
}
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
.await;
let accepted_items = match sess
match sess
.steer_input(
items.clone(),
additional_context.clone(),
@@ -242,7 +228,6 @@ pub(super) async fn user_input_or_turn_inner(
{
Ok(_) => {
current_context.session_telemetry.user_prompt(&items);
Some(items)
}
Err(SteerInputError::NoActiveTurn(items)) => {
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
@@ -256,7 +241,6 @@ pub(super) async fn user_input_or_turn_inner(
Some(sess.mcp_elicitation_reviewer()),
)
.await;
let accepted_items = items.clone();
let additional_context_input = {
let mut state = sess.state.lock().await;
state.additional_context.merge(additional_context)
@@ -278,7 +262,6 @@ pub(super) async fn user_input_or_turn_inner(
crate::tasks::RegularTask::new(),
)
.await;
Some(accepted_items)
}
Err(err) => {
sess.send_event_raw(Event {
@@ -286,33 +269,7 @@ pub(super) async fn user_input_or_turn_inner(
msg: EventMsg::Error(err.to_error_event()),
})
.await;
None
}
};
if let (Some(items), Some(())) = (accepted_items, mirror_user_text_to_realtime) {
self::mirror_user_text_to_realtime(sess, &items).await;
}
}
async fn mirror_user_text_to_realtime(sess: &Arc<Session>, items: &[UserInput]) {
let text = UserMessageItem::new(items).message();
if text.is_empty() {
return;
}
let text = if sess.conversation.is_running_v2().await {
prefix_realtime_v2_text(text, REALTIME_USER_TEXT_PREFIX)
} else {
text
};
let text = truncate_realtime_text_to_token_budget(&text, REALTIME_TURN_TOKEN_BUDGET);
if text.is_empty() {
return;
}
if sess.conversation.running_state().await.is_none() {
return;
}
if let Err(err) = sess.conversation.text_in(text).await {
debug!("failed to mirror user text to realtime conversation: {err}");
}
}
-1
View File
@@ -1132,7 +1132,6 @@ impl Session {
additional_context: Default::default(),
thread_settings: Default::default(),
},
/*mirror_user_text_to_realtime*/ None,
/*client_user_message_id*/ None,
)
.await;