mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: let steer interrupt wait_agent (#28341)
## Why `wait_agent` can block for a long timeout while waiting for sub-agent mailbox activity. Although same-turn user steer is accepted during that tool call, the input remains pending until the wait returns, so an explicit request to change direction can appear unresponsive. ## What changed - Notify active `wait_agent` calls when user input is steered into the current turn. - Check for already-pending steer input when subscribing so input that races with tool startup is not missed. - Distinguish mailbox activity, steered input, and timeout outcomes, returning `Wait interrupted by new input.` for the steer path. - Update the `wait_agent` tool description to document the early-return behavior. ## Testing - `just test -p codex-core input_queue_` - `just test -p codex-core wait_agent` The coverage includes steer notification before and after subscription, plus an end-to-end test that verifies the interrupted wait result and steered user input are both included exactly once in the follow-up model request.
This commit is contained in:
@@ -19,6 +19,12 @@ pub(crate) enum TurnInput {
|
||||
InterAgentCommunication(InterAgentCommunication),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum InputQueueActivity {
|
||||
Mailbox,
|
||||
Steer,
|
||||
}
|
||||
|
||||
/// Turn-local pending input storage owned by the input queue flow.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TurnInputQueue {
|
||||
@@ -27,25 +33,40 @@ pub(crate) struct TurnInputQueue {
|
||||
|
||||
/// Session-scoped pending input storage and active-turn mailbox delivery coordination.
|
||||
pub(crate) struct InputQueue {
|
||||
mailbox_tx: watch::Sender<()>,
|
||||
activity_tx: watch::Sender<InputQueueActivity>,
|
||||
mailbox_pending_mails: Mutex<VecDeque<InterAgentCommunication>>,
|
||||
}
|
||||
|
||||
impl InputQueue {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (mailbox_tx, _) = watch::channel(());
|
||||
let (activity_tx, _) = watch::channel(InputQueueActivity::Mailbox);
|
||||
Self {
|
||||
mailbox_tx,
|
||||
activity_tx,
|
||||
mailbox_pending_mails: Mutex::new(VecDeque::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribe_mailbox(&self) -> watch::Receiver<()> {
|
||||
let mut mailbox_rx = self.mailbox_tx.subscribe();
|
||||
if self.has_pending_mailbox_items().await {
|
||||
mailbox_rx.mark_changed();
|
||||
}
|
||||
mailbox_rx
|
||||
pub(crate) async fn subscribe_activity(
|
||||
&self,
|
||||
turn_state: Option<&Mutex<TurnState>>,
|
||||
) -> (
|
||||
watch::Receiver<InputQueueActivity>,
|
||||
Option<InputQueueActivity>,
|
||||
) {
|
||||
let activity_rx = self.activity_tx.subscribe();
|
||||
let has_pending_steer = if let Some(turn_state) = turn_state {
|
||||
turn_state.lock().await.pending_input.has_user_input()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let pending_activity = if has_pending_steer {
|
||||
Some(InputQueueActivity::Steer)
|
||||
} else if self.has_pending_mailbox_items().await {
|
||||
Some(InputQueueActivity::Mailbox)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(activity_rx, pending_activity)
|
||||
}
|
||||
|
||||
pub(crate) async fn enqueue_mailbox_communication(
|
||||
@@ -56,7 +77,7 @@ impl InputQueue {
|
||||
.lock()
|
||||
.await
|
||||
.push_back(communication);
|
||||
self.mailbox_tx.send_replace(());
|
||||
self.activity_tx.send_replace(InputQueueActivity::Mailbox);
|
||||
}
|
||||
|
||||
pub(crate) async fn has_pending_mailbox_items(&self) -> bool {
|
||||
@@ -146,9 +167,12 @@ impl InputQueue {
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: Vec<TurnInput>,
|
||||
) {
|
||||
let mut turn_state = turn_state.lock().await;
|
||||
turn_state.pending_input.items.extend(input);
|
||||
turn_state.accept_mailbox_delivery_for_current_turn();
|
||||
{
|
||||
let mut turn_state = turn_state.lock().await;
|
||||
turn_state.pending_input.items.extend(input);
|
||||
turn_state.accept_mailbox_delivery_for_current_turn();
|
||||
}
|
||||
self.activity_tx.send_replace(InputQueueActivity::Steer);
|
||||
}
|
||||
|
||||
pub(crate) async fn extend_pending_input_for_turn_state(
|
||||
@@ -228,6 +252,14 @@ impl InputQueue {
|
||||
}
|
||||
}
|
||||
|
||||
impl TurnInputQueue {
|
||||
fn has_user_input(&self) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.any(|input| matches!(input, TurnInput::UserInput { .. }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -252,7 +284,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn input_queue_notifies_mailbox_subscribers() {
|
||||
let input_queue = InputQueue::new();
|
||||
let mut mailbox_rx = input_queue.subscribe_mailbox().await;
|
||||
let (mut activity_rx, pending_activity) =
|
||||
input_queue.subscribe_activity(/*turn_state*/ None).await;
|
||||
assert_eq!(pending_activity, None);
|
||||
|
||||
input_queue
|
||||
.enqueue_mailbox_communication(make_mail(
|
||||
@@ -271,7 +305,59 @@ mod tests {
|
||||
))
|
||||
.await;
|
||||
|
||||
mailbox_rx.changed().await.expect("mailbox update");
|
||||
activity_rx.changed().await.expect("mailbox update");
|
||||
assert_eq!(
|
||||
*activity_rx.borrow_and_update(),
|
||||
InputQueueActivity::Mailbox
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn input_queue_notifies_steer_subscribers() {
|
||||
let input_queue = InputQueue::new();
|
||||
let turn_state = Mutex::new(TurnState::default());
|
||||
let (mut activity_rx, pending_activity) =
|
||||
input_queue.subscribe_activity(Some(&turn_state)).await;
|
||||
assert_eq!(pending_activity, None);
|
||||
|
||||
input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
&turn_state,
|
||||
vec![TurnInput::UserInput {
|
||||
content: vec![UserInput::Text {
|
||||
text: "steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
client_id: None,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
|
||||
activity_rx.changed().await.expect("steer update");
|
||||
assert_eq!(*activity_rx.borrow_and_update(), InputQueueActivity::Steer);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn input_queue_reports_already_pending_steer() {
|
||||
let input_queue = InputQueue::new();
|
||||
let turn_state = Mutex::new(TurnState::default());
|
||||
input_queue
|
||||
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
&turn_state,
|
||||
vec![TurnInput::UserInput {
|
||||
content: vec![UserInput::Text {
|
||||
text: "already pending".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
client_id: None,
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_activity_rx, pending_activity) =
|
||||
input_queue.subscribe_activity(Some(&turn_state)).await;
|
||||
|
||||
assert_eq!(pending_activity, Some(InputQueueActivity::Steer));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -220,6 +220,7 @@ use self::config_lock::validate_config_lock_if_configured;
|
||||
#[cfg(test)]
|
||||
use self::handlers::submission_dispatch_span;
|
||||
use self::handlers::submission_loop;
|
||||
pub(crate) use self::input_queue::InputQueueActivity;
|
||||
pub(crate) use self::input_queue::TurnInput;
|
||||
pub(crate) use self::input_queue::TurnInputQueue;
|
||||
use self::review::spawn_review_thread;
|
||||
|
||||
Reference in New Issue
Block a user