mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
tui: queue follow-ups during manual /compact (#15259)
## Summary - queue input after the user submits `/compact` until that manual compact turn ends - mirror the same behavior in the app-server TUI - add regressions for input queued before compact starts and while it is running Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
54801634e1
commit
e838645fa2
+70
-13
@@ -186,9 +186,41 @@ mod rollout_reconstruction_tests;
|
||||
pub enum SteerInputError {
|
||||
NoActiveTurn(Vec<UserInput>),
|
||||
ExpectedTurnMismatch { expected: String, actual: String },
|
||||
ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind },
|
||||
EmptyInput,
|
||||
}
|
||||
|
||||
impl SteerInputError {
|
||||
fn to_error_event(&self) -> ErrorEvent {
|
||||
match self {
|
||||
Self::NoActiveTurn(_) => ErrorEvent {
|
||||
message: "no active turn to steer".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
},
|
||||
Self::ExpectedTurnMismatch { expected, actual } => ErrorEvent {
|
||||
message: format!("expected active turn id `{expected}` but found `{actual}`"),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
},
|
||||
Self::ActiveTurnNotSteerable { turn_kind } => {
|
||||
let turn_kind_label = match turn_kind {
|
||||
NonSteerableTurnKind::Review => "review",
|
||||
NonSteerableTurnKind::Compact => "compact",
|
||||
};
|
||||
ErrorEvent {
|
||||
message: format!("cannot steer a {turn_kind_label} turn"),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: *turn_kind,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Self::EmptyInput => ErrorEvent {
|
||||
message: "input must not be empty".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes from the previous real user turn.
|
||||
///
|
||||
/// Conceptually this is the same role that `previous_model` used to fill, but
|
||||
@@ -333,6 +365,7 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::NonSteerableTurnKind;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_readiness::Readiness;
|
||||
@@ -3859,6 +3892,21 @@ impl Session {
|
||||
});
|
||||
}
|
||||
|
||||
match active_turn.tasks.first().map(|(_, task)| task.kind) {
|
||||
Some(crate::state::TaskKind::Regular) => {}
|
||||
Some(crate::state::TaskKind::Review) => {
|
||||
return Err(SteerInputError::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Review,
|
||||
});
|
||||
}
|
||||
Some(crate::state::TaskKind::Compact) => {
|
||||
return Err(SteerInputError::ActiveTurnNotSteerable {
|
||||
turn_kind: NonSteerableTurnKind::Compact,
|
||||
});
|
||||
}
|
||||
None => return Err(SteerInputError::NoActiveTurn(input)),
|
||||
}
|
||||
|
||||
let mut turn_state = active_turn.turn_state.lock().await;
|
||||
turn_state.push_pending_input(input.into());
|
||||
Ok(active_turn_id.clone())
|
||||
@@ -4526,26 +4574,35 @@ mod handlers {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id, updates).await else {
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
|
||||
// new_turn_with_sub_id already emits the error event.
|
||||
return;
|
||||
};
|
||||
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
|
||||
.await;
|
||||
current_context.session_telemetry.user_prompt(&items);
|
||||
|
||||
// Attempt to inject input into current task.
|
||||
if let Err(SteerInputError::NoActiveTurn(items)) =
|
||||
sess.steer_input(items, /*expected_turn_id*/ None).await
|
||||
match sess
|
||||
.steer_input(items.clone(), /*expected_turn_id*/ None)
|
||||
.await
|
||||
{
|
||||
sess.refresh_mcp_servers_if_requested(¤t_context)
|
||||
Ok(_) => current_context.session_telemetry.user_prompt(&items),
|
||||
Err(SteerInputError::NoActiveTurn(items)) => {
|
||||
current_context.session_telemetry.user_prompt(&items);
|
||||
sess.refresh_mcp_servers_if_requested(¤t_context)
|
||||
.await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(¤t_context),
|
||||
items,
|
||||
crate::tasks::RegularTask::new(),
|
||||
)
|
||||
.await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(¤t_context),
|
||||
items,
|
||||
crate::tasks::RegularTask::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
sess.send_event_raw(Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(err.to_error_event()),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::NonSteerableTurnKind;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
@@ -4507,6 +4508,43 @@ async fn steer_input_enforces_expected_turn_id() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steer_input_rejects_non_regular_turns() {
|
||||
for (task_kind, turn_kind) in [
|
||||
(TaskKind::Review, NonSteerableTurnKind::Review),
|
||||
(TaskKind::Compact, NonSteerableTurnKind::Compact),
|
||||
] {
|
||||
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let input = vec![UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
let turn_context = sess.new_default_turn_with_sub_id("turn".to_string()).await;
|
||||
sess.spawn_task(
|
||||
turn_context,
|
||||
input,
|
||||
NeverEndingTask {
|
||||
kind: task_kind,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let steer_input = vec![UserInput::Text {
|
||||
text: "steer".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
let err = sess
|
||||
.steer_input(steer_input, /*expected_turn_id*/ None)
|
||||
.await
|
||||
.expect_err("steering a non-regular turn should fail");
|
||||
|
||||
assert_eq!(err, SteerInputError::ActiveTurnNotSteerable { turn_kind });
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steer_input_returns_active_turn_id() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
Reference in New Issue
Block a user