mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Preserve steer input as user input (#23405)
## Why Steered input was queued as a `ResponseInputItem`, then parsed back into a user message before recording. That path loses information that only exists on `UserInput`, such as UI text elements. This change keeps turn-local pending input typed as either original `UserInput` or existing response items, so steered user input reaches user-message recording without being reconstructed from a response item. ## What changed - Add `TurnInput` for active-turn pending input. - Queue `Session::steer_input` as `TurnInput::UserInput`. - Run pending-input hook inspection only for `TurnInput::UserInput`. - Process drained pending input item by item: accepted items are recorded, blocked items append hook context and are skipped. - Remove the pending-input prepend/requeue path. ## Validation - `just fmt` - `just fix -p codex-core` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib session::tests::task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input -- --nocapture` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib steer_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --lib pending_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core --test all pending_input` - `RUST_MIN_STACK=16777216 cargo test -p codex-core` (unit tests passed: 1835 passed, 0 failed, 4 ignored; integration `all` target failed due missing helper binaries such as `codex`/`test_stdio_server` plus unrelated MCP/search/code-mode expectations)
This commit is contained in:
@@ -3,15 +3,22 @@ use crate::state::MailboxDeliveryPhase;
|
||||
use crate::state::TurnState;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum TurnInput {
|
||||
UserInput(Vec<UserInput>),
|
||||
ResponseInputItem(ResponseInputItem),
|
||||
}
|
||||
|
||||
/// Turn-local pending input storage owned by the input queue flow.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TurnInputQueue {
|
||||
items: Vec<ResponseInputItem>,
|
||||
items: Vec<TurnInput>,
|
||||
}
|
||||
|
||||
/// Session-scoped pending input storage and active-turn mailbox delivery coordination.
|
||||
@@ -151,7 +158,7 @@ impl InputQueue {
|
||||
pub(super) async fn push_pending_input_and_accept_mailbox_delivery_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: ResponseInputItem,
|
||||
input: TurnInput,
|
||||
) {
|
||||
let mut turn_state = turn_state.lock().await;
|
||||
turn_state.pending_input.items.push(input);
|
||||
@@ -161,7 +168,7 @@ impl InputQueue {
|
||||
pub(crate) async fn extend_pending_input_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
input: Vec<ResponseInputItem>,
|
||||
input: Vec<TurnInput>,
|
||||
) {
|
||||
turn_state.lock().await.pending_input.items.extend(input);
|
||||
}
|
||||
@@ -169,7 +176,7 @@ impl InputQueue {
|
||||
pub(crate) async fn take_pending_input_for_turn_state(
|
||||
&self,
|
||||
turn_state: &Mutex<TurnState>,
|
||||
) -> Vec<ResponseInputItem> {
|
||||
) -> Vec<TurnInput> {
|
||||
turn_state.lock().await.pending_input.items.split_off(0)
|
||||
}
|
||||
|
||||
@@ -185,43 +192,20 @@ impl InputQueue {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(active_turn) => {
|
||||
active_turn
|
||||
.turn_state
|
||||
.lock()
|
||||
.await
|
||||
.pending_input
|
||||
.items
|
||||
.extend(input);
|
||||
self.extend_pending_input_for_turn_state(
|
||||
active_turn.turn_state.as_ref(),
|
||||
input
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem)
|
||||
.collect(),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
)]
|
||||
pub(crate) async fn prepend_pending_input(
|
||||
&self,
|
||||
active_turn: &Mutex<Option<ActiveTurn>>,
|
||||
mut input: Vec<ResponseInputItem>,
|
||||
) -> Result<(), ()> {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(active_turn) => {
|
||||
let mut turn_state = active_turn.turn_state.lock().await;
|
||||
if !input.is_empty() {
|
||||
let pending_input = &mut turn_state.pending_input;
|
||||
input.append(&mut pending_input.items);
|
||||
pending_input.items = input;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
@@ -229,7 +213,7 @@ impl InputQueue {
|
||||
pub(crate) async fn get_pending_input(
|
||||
&self,
|
||||
active_turn: &Mutex<Option<ActiveTurn>>,
|
||||
) -> Vec<ResponseInputItem> {
|
||||
) -> Vec<TurnInput> {
|
||||
let (pending_input, accepts_mailbox_delivery) = {
|
||||
let mut active = active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
@@ -246,11 +230,13 @@ impl InputQueue {
|
||||
if !accepts_mailbox_delivery {
|
||||
return pending_input;
|
||||
}
|
||||
let mailbox_items = self.drain_mailbox_input_items().await;
|
||||
let mailbox_items = self
|
||||
.drain_mailbox_input_items()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(TurnInput::ResponseInputItem);
|
||||
if pending_input.is_empty() {
|
||||
mailbox_items
|
||||
} else if mailbox_items.is_empty() {
|
||||
pending_input
|
||||
mailbox_items.collect()
|
||||
} else {
|
||||
let mut pending_input = pending_input;
|
||||
pending_input.extend(mailbox_items);
|
||||
|
||||
Reference in New Issue
Block a user