Add opt-in code-mode yielding on new user input (#48135)

## Why

User input queued during a long-running code-mode `exec` or `wait` can remain pending until the call returns. Allow these calls to yield early so Codex can receive the new input while the cell continues running.

## What changed

- Add the `instant_interrupt` feature flag, disabled by default.
- Watch queued user input for each sampling request and pass a shared preemption signal to code-mode `exec` and `wait` calls, including calls received later in the same response.
- Yield running cell IDs without stopping the cells, allowing subsequent `wait` calls to collect their results.

## Testing

Add integration coverage for enabled and disabled behavior, repeated steering during `exec` and `wait`, input deferred during compaction, and later calls in the same response. Verify that direct tool results and queued user messages are preserved, and add a scenario snapshot showing continuation after a cell yields.

GitOrigin-RevId: e33466a980d98e113bfb6c6a45c59de0cc7b3c1d
This commit is contained in:
pakrym-oai
2026-09-25 16:28:36 +00:00
committed by copyberry
parent d9275a63ed
commit 1bf73324ca
13 changed files with 1011 additions and 6 deletions
+31
View File
@@ -14,6 +14,8 @@ use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
static PENDING_MAILBOX_MESSAGES: Gauge = Gauge::new("core.mailbox.pending");
@@ -215,6 +217,29 @@ impl InputQueue {
})
}
/// Signal once a user message is queued for this sampling request.
pub(crate) async fn watch_user_input(
&self,
active_turn: &Mutex<Option<ActiveTurn>>,
sub_id: &str,
interrupt: CancellationToken,
) -> Option<AbortOnDropHandle<()>> {
let turn_state = self.turn_state_for_sub_id(active_turn, sub_id).await?;
// Subscribe before inspecting the queue so an arrival cannot be missed.
let mut activity = self.activity_tx.subscribe();
Some(AbortOnDropHandle::new(tokio::spawn(async move {
loop {
if turn_state.lock().await.pending_input.has_user_input() {
interrupt.cancel();
return;
}
if activity.changed().await.is_err() {
return;
}
}
})))
}
/// Clear any pending waiters and input buffered for the current turn.
pub(crate) async fn clear_pending(&self, active_turn: &ActiveTurn) {
let mut turn_state = active_turn.turn_state.lock().await;
@@ -363,6 +388,12 @@ impl InputQueue {
}
impl TurnInputQueue {
fn has_user_input(&self) -> bool {
self.items
.iter()
.any(|input| matches!(input, TurnInput::UserInput { .. }))
}
pub(crate) fn is_empty(&self) -> bool {
self.items.is_empty()
}