Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Traut
4386ba2eda Treat user shell turns as non-steerable 2026-04-16 22:21:42 -07:00
28 changed files with 138 additions and 38 deletions

View File

@@ -111,6 +111,7 @@ pub enum TurnSteerRejectionReason {
ExpectedTurnMismatch,
NonSteerableReview,
NonSteerableCompact,
NonSteerableUserShell,
EmptyInput,
InputTooLarge,
}
@@ -137,6 +138,7 @@ pub enum TurnSteerRequestError {
ExpectedTurnMismatch,
NonSteerableReview,
NonSteerableCompact,
NonSteerableUserShell,
}
#[derive(Clone, Copy, Debug)]
@@ -152,6 +154,7 @@ impl From<TurnSteerRequestError> for TurnSteerRejectionReason {
TurnSteerRequestError::ExpectedTurnMismatch => Self::ExpectedTurnMismatch,
TurnSteerRequestError::NonSteerableReview => Self::NonSteerableReview,
TurnSteerRequestError::NonSteerableCompact => Self::NonSteerableCompact,
TurnSteerRequestError::NonSteerableUserShell => Self::NonSteerableUserShell,
}
}
}

View File

@@ -528,7 +528,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -1930,7 +1930,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -5813,7 +5813,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -9963,7 +9963,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -2430,7 +2430,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -6735,7 +6735,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -115,7 +115,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -140,7 +140,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -138,7 +138,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -524,7 +524,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -199,7 +199,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -615,7 +615,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -199,7 +199,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -615,7 +615,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -199,7 +199,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -615,7 +615,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -141,7 +141,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -550,7 +550,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -138,7 +138,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -524,7 +524,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -138,7 +138,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -524,7 +524,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -138,7 +138,7 @@
},
{
"additionalProperties": false,
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.",
"properties": {
"activeTurnNotSteerable": {
"properties": {
@@ -524,7 +524,8 @@
"NonSteerableTurnKind": {
"enum": [
"review",
"compact"
"compact",
"userShell"
],
"type": "string"
},

View File

@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NonSteerableTurnKind = "review" | "compact";
export type NonSteerableTurnKind = "review" | "compact" | "userShell";

View File

@@ -139,6 +139,7 @@ macro_rules! v2_enum_from_core {
pub enum NonSteerableTurnKind {
Review,
Compact,
UserShell,
}
/// This translation layer make sure that we expose codex error code in camel case.
@@ -181,7 +182,7 @@ pub enum CodexErrorInfo {
http_status_code: Option<u16>,
},
/// Returned when `turn/start` or `turn/steer` is submitted while the current active turn
/// cannot accept same-turn steering, for example `/review` or manual `/compact`.
/// cannot accept same-turn steering, for example `/review`, manual `/compact`, or a user shell command.
ActiveTurnNotSteerable {
#[serde(rename = "turnKind")]
#[ts(rename = "turnKind")]
@@ -228,6 +229,7 @@ impl From<CoreNonSteerableTurnKind> for NonSteerableTurnKind {
match value {
CoreNonSteerableTurnKind::Review => Self::Review,
CoreNonSteerableTurnKind::Compact => Self::Compact,
CoreNonSteerableTurnKind::UserShell => Self::UserShell,
}
}
}

View File

@@ -1058,7 +1058,7 @@ There are additional item-specific events:
- `ResponseStreamDisconnected { httpStatusCode? }`: disconnect of the response SSE stream in the middle of a turn before completion
- `ResponseTooManyFailedAttempts { httpStatusCode? }`
- `ActiveTurnNotSteerable { turnKind }`: `turn/start` or `turn/steer` was submitted while the
current active turn was not steerable, for example `/review` or manual `/compact`
current active turn was not steerable, for example `/review`, manual `/compact`, or a user shell command
- `BadRequest`
- `Unauthorized`
- `SandboxError`

View File

@@ -7061,6 +7061,10 @@ impl CodexMessageProcessor {
"cannot steer a compact turn".to_string(),
TurnSteerRequestError::NonSteerableCompact,
),
codex_protocol::protocol::NonSteerableTurnKind::UserShell => (
"cannot steer a user shell turn".to_string(),
TurnSteerRequestError::NonSteerableUserShell,
),
};
let error = TurnError {
message: message.clone(),

View File

@@ -216,6 +216,7 @@ impl SteerInputError {
let turn_kind_label = match turn_kind {
NonSteerableTurnKind::Review => "review",
NonSteerableTurnKind::Compact => "compact",
NonSteerableTurnKind::UserShell => "user shell",
};
ErrorEvent {
message: format!("cannot steer a {turn_kind_label} turn"),
@@ -2787,6 +2788,11 @@ impl Session {
turn_kind: NonSteerableTurnKind::Compact,
});
}
Some(crate::state::TaskKind::UserShell) => {
return Err(SteerInputError::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::UserShell,
});
}
None => return Err(SteerInputError::NoActiveTurn(input)),
}

View File

@@ -5391,6 +5391,7 @@ async fn steer_input_rejects_non_regular_turns() {
for (task_kind, turn_kind) in [
(TaskKind::Review, NonSteerableTurnKind::Review),
(TaskKind::Compact, NonSteerableTurnKind::Compact),
(TaskKind::UserShell, NonSteerableTurnKind::UserShell),
] {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {

View File

@@ -64,6 +64,7 @@ pub(crate) enum TaskKind {
Regular,
Review,
Compact,
UserShell,
}
pub(crate) struct RunningTask {

View File

@@ -63,7 +63,7 @@ impl UserShellCommandTask {
impl SessionTask for UserShellCommandTask {
fn kind(&self) -> TaskKind {
TaskKind::Regular
TaskKind::UserShell
}
fn span_name(&self) -> &'static str {

View File

@@ -1845,6 +1845,7 @@ pub enum AgentStatus {
pub enum NonSteerableTurnKind {
Review,
Compact,
UserShell,
}
/// Codex errors that we expose to clients.

View File

@@ -1027,6 +1027,71 @@ async fn bang_shell_command_submits_run_user_shell_command_in_app_server_tui() {
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
}
#[tokio::test]
async fn user_shell_steer_rejection_queues_follow_up_until_shell_turn_completes() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.thread_id = Some(ThreadId::new());
chat.handle_codex_event(Event {
id: "shell-turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "shell-turn".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
});
chat.submit_user_message(UserMessage::from("hi"));
assert_eq!(chat.pending_steers.len(), 1);
match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "hi".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected running-turn steer submit, got {other:?}"),
}
chat.handle_codex_event(Event {
id: "shell-steer-rejected".into(),
msg: EventMsg::Error(ErrorEvent {
message: "cannot steer a user shell turn".to_string(),
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
turn_kind: NonSteerableTurnKind::UserShell,
}),
}),
});
assert!(chat.pending_steers.is_empty());
assert_eq!(chat.queued_user_message_texts(), vec!["hi"]);
assert!(drain_insert_history(&mut rx).is_empty());
chat.handle_codex_event(Event {
id: "shell-turn-complete".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "shell-turn".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "hi".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected queued follow-up submit, got {other:?}"),
}
}
#[tokio::test]
async fn disabled_slash_command_while_task_running_snapshot() {
// Build a chat widget and simulate an active task