Queue Realtime V2 response.create while active (#17306)

Builds on #17264.

- queues Realtime V2 `response.create` while an active response is open,
then flushes it after `response.done` or `response.cancelled`
- requests `response.create` after background agent final output and
steering acknowledgements
- adds app-server integration coverage for all `response.create` paths

Validation:
- `just fmt`
- `cargo check -p codex-app-server --tests`
- `git diff --check`
- CI green

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-10 09:09:13 -07:00
committed by GitHub
Unverified
parent 88165e179a
commit 2e81eac004
7 changed files with 454 additions and 40 deletions
+113 -20
View File
@@ -68,6 +68,8 @@ const REALTIME_V2_PROGRESS_UPDATE_SUFFIX: &str =
"\n\nUpdate from background agent (task hasn't finished yet):";
const REALTIME_V2_STEER_ACKNOWLEDGEMENT: &str =
"This was sent to steer the previous background agent task.";
const REALTIME_ACTIVE_RESPONSE_ERROR_PREFIX: &str =
"Conversation already has an active response in progress:";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RealtimeConversationEnd {
@@ -117,6 +119,68 @@ struct OutputAudioState {
audio_end_ms: u32,
}
#[derive(Default)]
struct RealtimeResponseCreateQueue {
active_default_response: bool,
pending_create: bool,
}
impl RealtimeResponseCreateQueue {
async fn request_create(
&mut self,
writer: &RealtimeWebsocketWriter,
events_tx: &Sender<RealtimeEvent>,
reason: &str,
) -> anyhow::Result<()> {
if self.active_default_response {
self.pending_create = true;
return Ok(());
}
self.send_create_now(writer, events_tx, reason).await
}
fn mark_started(&mut self) {
self.active_default_response = true;
}
async fn mark_finished(
&mut self,
writer: &RealtimeWebsocketWriter,
events_tx: &Sender<RealtimeEvent>,
reason: &str,
) -> anyhow::Result<()> {
self.active_default_response = false;
if !self.pending_create {
return Ok(());
}
self.pending_create = false;
self.send_create_now(writer, events_tx, reason).await
}
async fn send_create_now(
&mut self,
writer: &RealtimeWebsocketWriter,
events_tx: &Sender<RealtimeEvent>,
reason: &str,
) -> anyhow::Result<()> {
if let Err(err) = writer.send_response_create().await {
let mapped_error = map_api_error(err);
let error_message = mapped_error.to_string();
if error_message.starts_with(REALTIME_ACTIVE_RESPONSE_ERROR_PREFIX) {
warn!("realtime response.create raced an active response; deferring");
self.active_default_response = true;
self.pending_create = true;
return Ok(());
}
warn!("failed to send {reason} response.create: {mapped_error}");
let _ = events_tx.send(RealtimeEvent::Error(error_message)).await;
return Err(mapped_error.into());
}
self.active_default_response = true;
Ok(())
}
}
struct RealtimeInputTask {
writer: RealtimeWebsocketWriter,
events: RealtimeWebsocketEvents,
@@ -871,12 +935,19 @@ fn spawn_realtime_input_task(input: RealtimeInputTask) -> JoinHandle<()> {
tokio::spawn(async move {
let mut output_audio_state: Option<OutputAudioState> = None;
let mut response_create_queue = RealtimeResponseCreateQueue::default();
loop {
let result = tokio::select! {
// Text typed by the user that should be sent into realtime.
user_text = user_text_rx.recv() => {
handle_user_text_input(user_text, &writer, &events_tx, session_kind)
handle_user_text_input(
user_text,
&writer,
&events_tx,
session_kind,
&mut response_create_queue,
)
.await
}
// Background agent progress or final output that should be sent back to realtime.
@@ -887,6 +958,7 @@ fn spawn_realtime_input_task(input: RealtimeInputTask) -> JoinHandle<()> {
&events_tx,
&handoff_state,
event_parser,
&mut response_create_queue,
)
.await
}
@@ -899,6 +971,7 @@ fn spawn_realtime_input_task(input: RealtimeInputTask) -> JoinHandle<()> {
&handoff_state,
session_kind,
&mut output_audio_state,
&mut response_create_queue,
)
.await
}
@@ -920,6 +993,7 @@ async fn handle_user_text_input(
writer: &RealtimeWebsocketWriter,
events_tx: &Sender<RealtimeEvent>,
session_kind: RealtimeSessionKind,
response_create_queue: &mut RealtimeResponseCreateQueue,
) -> anyhow::Result<()> {
let text = text.context("user text input channel closed")?;
@@ -934,14 +1008,9 @@ async fn handle_user_text_input(
match session_kind {
RealtimeSessionKind::V1 => {}
RealtimeSessionKind::V2 => {
if let Err(err) = writer.send_response_create().await {
let mapped_error = map_api_error(err);
warn!("failed to send text response.create: {mapped_error}");
let _ = events_tx
.send(RealtimeEvent::Error(mapped_error.to_string()))
.await;
return Err(mapped_error.into());
}
response_create_queue
.request_create(writer, events_tx, "text")
.await?;
}
}
Ok(())
@@ -953,6 +1022,7 @@ async fn handle_handoff_output(
events_tx: &Sender<RealtimeEvent>,
handoff_state: &RealtimeHandoffState,
event_parser: RealtimeEventParser,
response_create_queue: &mut RealtimeResponseCreateQueue,
) -> anyhow::Result<()> {
let handoff_output = handoff_output.context("handoff output channel closed")?;
@@ -1000,7 +1070,9 @@ async fn handle_handoff_output(
{
Err(err)
} else {
writer.send_response_create().await
return response_create_queue
.request_create(writer, events_tx, "handoff")
.await;
}
}
},
@@ -1023,6 +1095,7 @@ async fn handle_realtime_server_event(
handoff_state: &RealtimeHandoffState,
session_kind: RealtimeSessionKind,
output_audio_state: &mut Option<OutputAudioState>,
response_create_queue: &mut RealtimeResponseCreateQueue,
) -> anyhow::Result<()> {
let event = match event {
Ok(Some(event)) => event,
@@ -1079,8 +1152,35 @@ async fn handle_realtime_server_event(
}
false
}
RealtimeEvent::ResponseCreated(_) => {
match session_kind {
RealtimeSessionKind::V1 => {}
RealtimeSessionKind::V2 => response_create_queue.mark_started(),
}
false
}
RealtimeEvent::ResponseCancelled(_) => {
*output_audio_state = None;
match session_kind {
RealtimeSessionKind::V1 => {}
RealtimeSessionKind::V2 => {
response_create_queue
.mark_finished(writer, events_tx, "deferred")
.await?;
}
}
false
}
RealtimeEvent::ResponseDone(_) => {
*output_audio_state = None;
match session_kind {
RealtimeSessionKind::V1 => {}
RealtimeSessionKind::V2 => {
response_create_queue
.mark_finished(writer, events_tx, "deferred")
.await?;
}
}
false
}
RealtimeEvent::HandoffRequested(handoff) => {
@@ -1111,16 +1211,9 @@ async fn handle_realtime_server_event(
.await;
return Err(mapped_error.into());
}
if let Err(err) = writer.send_response_create().await {
let mapped_error = map_api_error(err);
warn!(
"failed to send handoff steering response.create: {mapped_error}"
);
let _ = events_tx
.send(RealtimeEvent::Error(mapped_error.to_string()))
.await;
return Err(mapped_error.into());
}
response_create_queue
.request_create(writer, events_tx, "handoff steering")
.await?;
}
None => {
*handoff_state.last_output_text.lock().await = None;